Complete AI context for the Service Portal and Castle Checkers deployment. An agent reading this can continue work from scratch.

Session History

DateWhat Happened
2026-07-03v2 complete rewrite. Connected frontend to Worker API. Real PBKDF2 auth. Config-driven public site. Service request wizard submits to API. All admin views (Requests, Clients, Appointments, Calendar, Invoices, Reports, Settings) connected. Full CRUD. Migration 0002 for Castle Checkers. Committed as 802a351f, not yet pushed/deployed.
Prior sessionsInitial scaffold built (React + Vite + Worker + D1 schema). Frontend was a disconnected demo โ€” all state in-memory, never called the API.

Infrastructure

ResourceValue
GitHub reposonantechai/caretaker-portal
Local pathE:\Claude_Projects\sonan-trackers\caretaker-portal-Claude\caretaker-portal\
Bash path/sessions/*/mnt/sonan-trackers/caretaker-portal-Claude/caretaker-portal/
Last pushed commitab86aea54db0061d9cd816791ef24ab92647f788
Latest commit (not pushed)802a351f47ee4bfd46ceb9b1210cab43938cc99e
Cloudflare accountsonantechai (ID: a7bd7d4b54bb7cb5907f8ddd45c3e5d9)

Castle Checkers Credentials

ItemValue
Worker namecastlecheckers-api
Worker envwrangler deploy --env castlecheckers
D1 database namecaretaker_db
D1 database IDc7fe5daa-1f2d-46ea-9939-7bee847591ab
Default admin passwordAdmin@2026 (set in migration 0002, must change after first login)
Password hash (PBKDF2, salt=SONAN_SP_2026)da004d5ed0e3915f754d8b79ba29c078aa4190a5d9e2d7bcd4188755fb9fb741
JWT_SECRETSet via npx wrangler secret put JWT_SECRET --env castlecheckers โ€” use random 32-byte hex
Subdomaincastlecheckers.sonandigital.com
VITE_API_URLhttps://castlecheckers-api.sonantechai.workers.dev

Auth Flow (How It Works)

Customer visits castlecheckers.sonandigital.com
  โ””โ”€ App.tsx: useEffect โ†’ api.getConfig() โ†’ GET /api/config
  โ””โ”€ Loads business name, service types, colors into AppContext
  โ””โ”€ Shows PublicSite (public, no auth)

Customer submits request:
  โ””โ”€ ServiceRequestWizard โ†’ api.submitRequest() โ†’ POST /api/requests
  โ””โ”€ Worker: INSERT into service_requests โ†’ returns { success, id }
  โ””โ”€ Admin sees it in Requests inbox

Admin clicks "Admin" link:
  โ””โ”€ Enters password โ†’ api.login() โ†’ POST /api/admin/login
  โ””โ”€ Worker: PBKDF2(password, 'SONAN_SP_2026', 100k, SHA-256) โ†’ compare hash
  โ””โ”€ Returns HMAC-signed token: base64(payload).signature
  โ””โ”€ AppContext stores token โ†’ triggers loadAll() โ†’ fetches all data
  โ””โ”€ Shows AdminApp with live data

All admin API calls:
  โ””โ”€ Header: Authorization: Bearer token
  โ””โ”€ Worker: split token โ†’ HMAC verify โ†’ check exp โ†’ run query
  โ””โ”€ Returns camelCase JSON (snake_case in DB โ†’ mapped in worker)

Password change:
  โ””โ”€ Settings page โ†’ POST /api/admin/password { currentPassword, newPassword }
  โ””โ”€ Worker: verify current, hash new, UPDATE admin_users SET password_hash

Generating a New Password Hash

Run in browser console (Chrome) or Node 18+:

// In Chrome console or Node 18+ (--input-type=module):
async function hashPassword(pass) {
  const salt = new TextEncoder().encode('SONAN_SP_2026');
  const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(pass), 'PBKDF2', false, ['deriveBits']);
  const bits = await crypto.subtle.deriveBits({name:'PBKDF2',hash:'SHA-256',salt,iterations:100000}, key, 256);
  return Array.from(new Uint8Array(bits)).map(b=>b.toString(16).padStart(2,'0')).join('');
}
hashPassword('YourNewPassword').then(console.log);
// Paste the output into migration or UPDATE admin_users SET password_hash = '...'

Adding Realtor (Next Client)

# 1. Create new D1 database
npx wrangler d1 create realtor_db
# Copy the database_id from output

# 2. Uncomment [env.realtor] in wrangler.toml and paste the database_id

# 3. Create migrations/0002_realtor_seed.sql with realtor-specific settings:
#    INSERT OR REPLACE INTO settings (key, value) VALUES
#      ('business_name', 'XYZ Realtor'),
#      ('service_types', '["Buyer Inquiry","Listing Request","Property Valuation","Rental Inquiry"]'),
#      ...

# 4. Apply migrations
npx wrangler d1 migrations apply realtor_db --remote --env realtor

# 5. Set secrets
npx wrangler secret put JWT_SECRET --env realtor

# 6. Deploy
npx wrangler deploy --env realtor

# 7. Create new Cloudflare Pages project pointing to same GitHub repo
#    Set VITE_API_URL = https://realtor-api.sonantechai.workers.dev
#    Add custom domain: xyz.sonandigital.com

Key Files to Know

FilePurpose
worker/index.tsEntire API โ€” 542 lines, all routes, auth helpers, D1 queries, camelCase mapping
migrations/0001_init.sqlFull DB schema
migrations/0002_settings_seed.sqlCastle Checkers settings + admin password hash
src/api.tsAll fetch() calls โ€” the only file that knows the API URL
src/context/AppContext.tsxAll app state + API-connected actions. No hardcoded data.
src/components/PublicSite.tsxConfig-driven public site; reads from state.config
src/components/ServiceRequestWizard.tsxMulti-step wizard; service types from getServiceTypes() hook
wrangler.tomlcastlecheckers env configured; realtor placeholder commented
_redirects/* /index.html 200 โ€” Cloudflare Pages SPA routing

snake_case โ†” camelCase Mapping

D1 stores snake_case column names. The Worker maps to camelCase before returning JSON. Frontend always uses camelCase. Example: full_name โ†’ fullName, appointment_date โ†’ date, created_at โ†’ createdAt.

If you add a new DB column, add the mapping in the corresponding map*() function in worker/index.ts.

Common Mistakes to Avoid

MistakeFix
Pushing from sandbox โ€” Cloudflare returns 403All wrangler and git push from Windows terminal only
Editing worker without redeployingPages auto-deploys frontend on push; Worker needs npx wrangler deploy --env castlecheckers
Forgetting JWT_SECRET secretIf JWT_SECRET is not set, all login attempts fail with 500. Set it before deploying.
FUSE corruption on large file editsWrite to /tmp first, hash from /tmp, use git plumbing โ€” see CLAUDE.md
Invoice items not showingWorker uses json_group_array SQLite function โ€” requires D1 compat date โ‰ฅ 2024-04-01 (already set in wrangler.toml)