Service Portal
Service Portal โ AI Handoff
Complete AI context for the Service Portal and Castle Checkers deployment. An agent reading this can continue work from scratch.
Session History
| Date | What Happened |
|---|---|
| 2026-07-03 | v2 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 sessions | Initial scaffold built (React + Vite + Worker + D1 schema). Frontend was a disconnected demo โ all state in-memory, never called the API. |
Infrastructure
| Resource | Value |
|---|---|
| GitHub repo | sonantechai/caretaker-portal |
| Local path | E:\Claude_Projects\sonan-trackers\caretaker-portal-Claude\caretaker-portal\ |
| Bash path | /sessions/*/mnt/sonan-trackers/caretaker-portal-Claude/caretaker-portal/ |
| Last pushed commit | ab86aea54db0061d9cd816791ef24ab92647f788 |
| Latest commit (not pushed) | 802a351f47ee4bfd46ceb9b1210cab43938cc99e |
| Cloudflare account | sonantechai (ID: a7bd7d4b54bb7cb5907f8ddd45c3e5d9) |
Castle Checkers Credentials
| Item | Value |
|---|---|
| Worker name | castlecheckers-api |
| Worker env | wrangler deploy --env castlecheckers |
| D1 database name | caretaker_db |
| D1 database ID | c7fe5daa-1f2d-46ea-9939-7bee847591ab |
| Default admin password | Admin@2026 (set in migration 0002, must change after first login) |
| Password hash (PBKDF2, salt=SONAN_SP_2026) | da004d5ed0e3915f754d8b79ba29c078aa4190a5d9e2d7bcd4188755fb9fb741 |
| JWT_SECRET | Set via npx wrangler secret put JWT_SECRET --env castlecheckers โ use random 32-byte hex |
| Subdomain | castlecheckers.sonandigital.com |
| VITE_API_URL | https://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
| File | Purpose |
|---|---|
worker/index.ts | Entire API โ 542 lines, all routes, auth helpers, D1 queries, camelCase mapping |
migrations/0001_init.sql | Full DB schema |
migrations/0002_settings_seed.sql | Castle Checkers settings + admin password hash |
src/api.ts | All fetch() calls โ the only file that knows the API URL |
src/context/AppContext.tsx | All app state + API-connected actions. No hardcoded data. |
src/components/PublicSite.tsx | Config-driven public site; reads from state.config |
src/components/ServiceRequestWizard.tsx | Multi-step wizard; service types from getServiceTypes() hook |
wrangler.toml | castlecheckers 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
| Mistake | Fix |
|---|---|
| Pushing from sandbox โ Cloudflare returns 403 | All wrangler and git push from Windows terminal only |
| Editing worker without redeploying | Pages auto-deploys frontend on push; Worker needs npx wrangler deploy --env castlecheckers |
| Forgetting JWT_SECRET secret | If JWT_SECRET is not set, all login attempts fail with 500. Set it before deploying. |
| FUSE corruption on large file edits | Write to /tmp first, hash from /tmp, use git plumbing โ see CLAUDE.md |
| Invoice items not showing | Worker uses json_group_array SQLite function โ requires D1 compat date โฅ 2024-04-01 (already set in wrangler.toml) |