AI-powered natural language scheduling app. Built for TestSprite Hackathon Season 3 β "Build the Loop".
App: https://timeora-alpha.vercel.app
| Resource | Link |
|---|---|
| Demo video | Watch on YouTube |
| X post | x.com/ArdinPrayoga/β¦ |
| Medium β Building Timeora with a Real Loop: How I Used TestSprite CLI in Hackathon Season 3 | Read on Medium |
The AI calendar chat understands Indonesian and English. It can create, query, reschedule, cancel, and find free slots from commands such as:
- "Jadwalkan meeting tim marketing besok jam 2 siang selama 45 menit"
- "Pindahkan standup ke jam 3 sore"
- "What do I have on Friday?"
- "Cari waktu kosong 2 jam besok"
OpenAI/OpenRouter is used when available. A deterministic bilingual parser takes over during provider outages and clearly marks the result as an offline parse.
The chat keeps session history, shows typing/confirmation feedback, returns structured event cards for calendar questions, and asks a clarification question when a cancel/reschedule command matches more than one event. Calendar mutations run through native backend tools and still require explicit confirmation.
- Weekly/day calendar with create, read, update, delete, drag, and resize
- Rich event details: description, meeting URL, priority, tags, and reminders
- Hover/tap event previews with safe meeting links and Gmail search links
- Right-click event actions plus Android-visible overflow actions for edit, delete, and Ask AI
- Conflict detection on create and update, including explained alternative slots
- Daily, weekday, weekly, and monthly recurring events
- Soft delete with one-click Undo
.icsexport for Google Calendar, Apple Calendar, and Outlook.icsimport with duplicate UID and conflict protection- Foreground browser notifications with in-app fallback reminders. Gmail support is intentionally limited to a pre-filled Gmail search link; automatic Gmail meeting-link extraction needs a connected Gmail OAuth scope and is not enabled in the standalone web app.
- Outgoing webhooks for event create, update, delete, and restore
- HMAC signatures, retries, SSRF protection, per-user limits, and sync logs
- Resend email notifications for event participants
- Encrypted provider token storage for Google, Zoom, Slack, Microsoft, and Notion
- Integrations settings UI at
/integrations
- Weekly workload, deep-work, and fragmentation insights
- One-click Block focus time and Spread load actions
- Day-by-hour availability heatmap with recommended free windows
Supabase email/password authentication includes refresh tokens. API access tokens require a valid signature, issuer, audience, expiry, and subject. Legacy HS256 tokens use the configured secret; ES256/RS256 tokens use the project JWKS.
| Layer | Technology |
|---|---|
| Frontend | Next.js 16 (App Router) + TypeScript + shadcn/ui + Framer Motion |
| Backend | FastAPI (Python 3.11) + asyncpg |
| Database | PostgreSQL via Supabase |
| Auth | Supabase Auth (email/password, JWT) |
| AI Parsing | OpenAI/OpenRouter + deterministic bilingual fallback |
| Frontend Hosting | Vercel |
| Backend Hosting | Railway |
| Testing & CI/CD | TestSprite CLI + GitHub Actions |
Built with a strict write β verify β fail β fix β verify loop against public
URLs (not localhost). Full chronological log, scorecard, and failure index:
LOOP.md (iterations #1β#32).
Timeora is a bilingual AI scheduling product where the TestSprite loop forced production decisions β forged-JWT rejection, dual DB failover on Railway, FullCalendar agent-hard UI, and locale correctness β proven by a 65/65 live suite plus 227 unit tests, not a one-shot demo.
| Metric | Value |
|---|---|
| Loop iterations | 32 |
| TestSprite live (cloud) | 65/65 (25 BE + 40 FE) |
| Backend unit | 152/152 |
| Frontend unit | 75/75 |
| Product bugs closed under the loop | 8+ |
| CI gate | Fail on non-pass TestSprite + deploy revision checks |
| # | Decision | Loop signal | Outcome |
|---|---|---|---|
| 1 | JWT security (#19) |
Forged JWT still returned 200 under MVP verify_signature: False |
Strict JWKS/HS256 claims; CI no longer soft-passes |
| 2 | DB production (#2, #7β#8) |
Live login / create-event failed with db: disconnected |
Session pooler + Supabase REST failover |
| 3 | Category drag (#21) |
Three cloud fails on persist/attribute before green | Stable data-timeora-* DOM + evaluate-click harness |
| 4 | Locale (#25β#26) |
EN UI got Indonesian assistant text via naive ,id match |
q-value locale parse + BE/FE re-verify |
| 5 | Deploy + CI honesty (#11, #19β#20, #23) |
Stale Railway, false-green gates, 502 report set | Root deploys, fixed-ID GHA, failure bundles on fail |
Detail and platform IDs for each case: LOOP.md β How TestSprite Changed Timeora.
| Layer | What it covers | Current result |
|---|---|---|
| Backend unit | JWT security, bilingual parsing, conflict ranking, recurrence, analytics, availability, ICS, integrations, event details, native assistant tools | 152/152 passed (local) |
| Frontend unit | API hardening, calendar actions, assistant chat, reminders, dialogs, session, i18n helpers | 75/75 passed (local Vitest) |
| Frontend static | ESLint + strict TypeScript | Passed |
| Frontend build | Next.js production bundle | Passed |
| TestSprite live suite | Cloud checks against the public app (not localhost) | 65/65 passed Β· 0 failed Β· 0 blocked Β· 0 draft |
| Split | Count | Status |
|---|---|---|
| Backend API tests | 25 | passed |
| Frontend browser tests | 40 | passed |
| Total | 65 | all green |
Representative live coverage (not every edge case β strong on core + major features):
- Auth & security β register/login, refresh, signed JWT / security smoke
- NL scheduling β hybrid EN/ID parse, conflict alternatives
- Assistant β create, query, reschedule, cancel, find free slot, confirm flows, voice control
- Calendar β E2E create, rich details, recurring (dialog + NL), soft-delete undo, event actions
- Export / integrations β
.icsexport paths, ICS import + webhook foundation - Analytics β weekly insights, block-focus action, availability heatmap
- Product UX β EN/ID language switch, landing bilingual demo, category/templates/agenda/theme/profile
Honest scope note: TestSprite is the live checker for the loop. It does not claim exhaustive coverage of every polish path (e.g. full OAuth providers, browser notification runtime, every mobile-only gesture). Those remain local unit tests and manual demo.
Judges can open artifacts in-repo (not only the TestSprite dashboard):
| Story | Path |
|---|---|
| Category drag failβfix | .testsprite/failure-category-v7/, v8/ |
| Templates / agenda | .testsprite/failure-template-v3/ β¦, .testsprite/failure-agenda-v2/ |
| Early FE / CORS | .testsprite/failure-frontend/ |
| Later run receipts | .testsprite/runs/ |
Full index: LOOP.md β Committed failure evidence.
- Live app β register/sign-in
- AI chat: create event (EN or ID) β confirm on calendar
- Soft-delete β Undo; optional category drag
- Language toggle EN β ID
LOOP.mdscorecard β case study #1 or #3 β open a failure folder
GitHub Actions runs local quality checks first, waits for the matching Vercel revision and a healthy Railway deployment, then executes fixed TestSprite backend and frontend test IDs. Failed or blocked TestSprite verdicts fail the workflow and trigger failure-bundle collection; they are not converted into false-green builds.
- Vercel frontend tracks
main. - Railway backend tracks
backend. - After backend fixes land on
main, fast-forwardbackendfrommainand pushbackendso production runs the same verified commit.
| Method | Path | Description |
|---|---|---|
GET |
/api/health |
Health check (includes DB status) |
POST |
/api/auth/register |
Register new user |
POST |
/api/auth/login |
Login (email + password) |
GET |
/api/events |
List user events |
POST |
/api/events |
Create event (with conflict detection) |
GET |
/api/events/{id} |
Get single event |
PUT |
/api/events/{id} |
Update event with conflict checking |
DELETE |
/api/events/{id} |
Soft-delete event |
POST |
/api/events/{id}/restore |
Restore a deleted event |
POST |
/api/parse |
Hybrid AI/offline natural-language parse |
POST |
/api/assistant |
Query, find slot, preview, and execute assistant actions |
POST |
/api/events/check-conflict |
Check time slot conflicts |
GET |
/api/analytics/week |
Weekly workload and insight actions |
GET |
/api/analytics/availability |
Availability heatmap and best slots |
POST |
/api/analytics/actions/block-focus |
Add a recommended focus block |
POST |
/api/analytics/actions/spread-load |
Rebalance the busiest weekday |
GET |
/api/export/ics |
Export the calendar as iCalendar |
POST |
/api/events/import-ics |
Import an iCalendar file |
GET |
/api/integrations |
List provider readiness and connection status |
PUT |
/api/integrations/{provider} |
Store an encrypted provider connection |
DELETE |
/api/integrations/{provider} |
Disconnect a provider |
GET |
/api/webhooks |
List outgoing webhook subscriptions |
POST |
/api/webhooks |
Register an outgoing webhook |
DELETE |
/api/webhooks/{id} |
Delete an outgoing webhook |
POST |
/api/auth/refresh |
Refresh an expired access token |
cd backend
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # macOS/Linux
pip install -r requirements.txt
cp .env.example .env # Fill in your env vars
# From the repository root, link Supabase once and apply pending migrations:
# npx --yes supabase@latest link --project-ref <project-ref>
# npx --yes supabase@latest db push
uvicorn app.main:app --reload --port 8000cd frontend
npm install
# Create .env.local and set NEXT_PUBLIC_API_URL
npm run devBackend (.env):
DATABASE_URL=postgresql://...
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_SERVICE_ROLE_KEY=...
SUPABASE_JWT_SECRET=...
OPENROUTER_API_KEY=...
OPENAI_API_KEY=...
OPENAI_MODEL=gpt-4o-mini
INTEGRATION_ENCRYPTION_KEY=...
INTEGRATION_SIGNING_KEY=...
INTEGRATION_RESEND_API_KEY=...
INTEGRATION_RESEND_FROM_EMAIL=Timeora <notifications@example.com>
INTEGRATION_EMAIL_NOTIFICATIONS_ENABLED=false
Frontend (.env.local):
NEXT_PUBLIC_API_URL=http://localhost:8000/api
# Backend
cd backend
python -m unittest discover -s tests -v
# Frontend
cd frontend
npm test
npm run lint
npx tsc --noEmit
npm run buildProject: fe31e397-bb11-4aae-af0f-2916b246b3f5 Β· suite target 65/65 passed.
testsprite auth status
# Full suite status (expect passed: 65, failed/blocked/draft: empty)
testsprite test list \
--project fe31e397-bb11-4aae-af0f-2916b246b3f5 \
--output json
testsprite test list --project fe31e397-bb11-4aae-af0f-2916b246b3f5 --status failed
testsprite test list --project fe31e397-bb11-4aae-af0f-2916b246b3f5 --status blocked
# Spot-check a frontend gate (example)
testsprite test run 5c7bac18-9569-4c14-af7f-17d3bb6d6909 \
--target-url https://timeora-alpha.vercel.app \
--wait --timeout 600 --output json| Field | Value |
|---|---|
| Hackathon | TestSprite Season 3 β "Build the Loop" |
| Creator | Bagus Ardin Prayoga |
| Account | bagusardinp@gmail.com |
| GitHub | github.com/bagusardin25/Timeora |
| Live app | timeora-alpha.vercel.app |
| TestSprite suite | 65/65 passed (25 backend + 40 frontend) on the live app |
| Demo video | youtu.be/amYxcmTNE5I |
| X post | x.com/ArdinPrayoga/β¦ |
| Loop log | LOOP.md Β· iterations #1β#32 |
| Medium write-up | Building Timeora with a Real Loop |
Built with β€οΈ and verified with TestSprite β because shipping without testing is just hoping.
