Removes resend npm dependency. Adds lib/notifications/mailer.ts with a
raw-HTTP Brevo wrapper (sendEmail + sendPasswordResetEmail). All three
notification files updated to use the new wrapper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Converts all 18 server component page files from Supabase client queries
to Drizzle ORM using asAdmin. Adds getSession() + redirect to the three
pages (hse/incidents, hse/incidents/[id], hse/dashboard) that lacked it.
Maps snake_case component prop shapes explicitly where required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Convert all 11 incident API routes from Supabase PostgREST to Drizzle
ORM with withUser/asAdmin/writeAuditLog patterns and RLS enforcement.
Only uploadEvidenceFile retains supabase client (Phase 5 storage work).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Convert app/api/admin/sites, trucks, users from Supabase PostgREST to
Drizzle ORM. All data ops use asAdmin(); all audit writes use
withUser(session.sub, tx => writeAuditLog(tx, ...)). Zero supabase
imports remain in the three files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Convert lib/settings.ts, lib/notifications/{in-app,email,capa-escalation,
effectiveness-recheck}.ts from Supabase PostgREST to Drizzle asAdmin queries.
Drop supabase arg from all call sites in app/api/ and cron routes. Rewrite
notification unit tests to mock @/lib/db/with-user instead of SupabaseClient.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Custom auth stack: bcryptjs password hashing (cost 10, GoTrue-compatible),
jose JWT session cookies (edge-safe, 8hr TTL), new API routes for
login/logout/reset/change-password, middleware rewritten to JWT-only
verification with no DB access. All 38 protected pages and API routes
migrated from supabase.auth.getUser() to getSession(). Supabase .from()
queries retained for Phase 4. lib/db/index.ts refactored to lazy Proxy
singleton to avoid module-level throw during Next.js build.
tsc: clean, build: clean, tests: 4/4 passed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds owner_notes to the GET select query and PATCH allowed fields list to enable reading and writing owner notes for CAPA actions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Verified CAPAs now show a Close button on the detail page for
HSE/admin, transitioning status to closed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
API routes were being redirected for non-admin roles (e.g. capa_owner
calling PATCH /api/capa/[id] got redirected to /capa-owner, breaking
all API calls from role-restricted users).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WMymkhHZiaYZtUeH9MEHZQ
- auth callback: remove debug redirect, handle both code (PKCE) and
token_hash+type (recovery/magic link) flows correctly
- capa PATCH: use admin client to bypass RLS for status updates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WMymkhHZiaYZtUeH9MEHZQ
Strips invite-by-email mode from admin user management UI and API.
POST /api/admin/users now always requires password parameter for
direct user creation. Removes mode toggle, conditional password field,
and invite logic branches. Simplifies user creation flow to single path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RkCfBY9L5y3SZ6uaKZzLCV
Prevent invite emails from falling back to localhost when NEXT_PUBLIC_APP_URL
is unset in production. Now explicitly checks and rejects with 503.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Scanning truck QR opens /ims/report?truck_id=<id>, which pre-selects
incident_type=transport and the truck in the form. TruckManager shows
"Report link / QR target" per truck, mirroring zones.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
- ALTER TYPE incident_type ADD VALUE 'transport' (isolated migration)
- trucks table with truck_no (unique), carrier, active, RLS mirroring sites
- incidents.truck_id UUID FK + index
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
Responsive variants (md:flex, md:hidden) were silently missing from the
Tailwind CSS bundle due to stale build cache — sidebar was always hidden.
Changed breakpoint to sm (640px) and use rm -rf .next before builds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
Single CSS rule in globals.css makes typed text gray-900 and placeholders
gray-500 across all 15 field-bearing components. Element selector specificity
(0,0,1) is lower than any existing Tailwind utility so no per-component
overrides are affected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
Migration copies ANTHROPIC_API_KEY value → DEEPSEEK_API_KEY (DeepSeek key
was saved before the rename), then deletes ANTHROPIC_API_KEY and
VOYAGE_API_KEY rows. ALLOWED_KEYS now only lists active provider keys.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
- embedText: call Gemini REST API (768-dim) instead of Voyage (1024-dim)
- Migration: drop+recreate incidents.embedding as vector(768), update
match_incidents function, swap VOYAGE_API_KEY → GOOGLE_AI_API_KEY in app_settings
- Settings UI: relabel to "Google AI API Key (Embeddings)"
- All call sites updated (incidents POST, similar GET)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
- uploadEvidenceFile: validate file type via file-type magic bytes, reject
client-supplied MIME, derive extension from detected type, upload ArrayBuffer
- getEscalationThreshold: use setUTCHours instead of setHours so date-only ISO
strings (always UTC midnight) compare consistently in any timezone
- Tests: mock file-type, update upload expectation to ArrayBuffer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
- middleware.ts: add /account to isSharedRoute so all roles can reach it
- components/account/change-password-form.tsx: re-auth with current password
then updateUser({password}) with client-side validation (length, match, diff)
- app/(protected)/account/page.tsx: dedicated account page, no role gate
- sidebar.tsx: Account link (all roles) above Logout in desktop footer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
- lib/claude/client.ts: replace Anthropic SDK with openai package pointed at DeepSeek baseURL
- 4 AI routes: port tool definitions, tool_choice, and output parsing to OpenAI function-calling format
- Drop thinking:{type:'adaptive'} (no DeepSeek equivalent); model string → deepseek-chat
- settings/route.ts: add DEEPSEEK_API_KEY to ALLOWED_KEYS
- migration: seed DEEPSEEK_API_KEY placeholder row in app_settings
- tests: update 3 AI route tests to mock createDeepSeekClient + OpenAI response shape
Voyage AI embedding path untouched (DeepSeek has no embeddings endpoint).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
- /api/incidents/[id]/similar: embedding backfill on a closed incident hit
the closure-lock trigger and turned the whole request into a 503; now
skips persistence for closed incidents (vector still used for the query)
- addenda: cap body at 5000 chars; include body text in audit_log entry
- admin users PATCH: 404 when target user does not exist (was silent ok)
- extract shared requireAdmin to lib/auth/require-admin.ts (was duplicated
in admin users + sites routes)
- extract escapeCsv/rowsToCsv to lib/csv.ts (was duplicated in dashboard
export route and lib/reports/jkkp8.ts)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
- README was untouched create-next-app boilerplate
- CLAUDE.md still claimed "Phase 0 not started"; all phases 0-6 complete
- .env.local.example was missing SUPABASE_SERVICE_ROLE_KEY, Resend, and
Meta WhatsApp variables added in later phases
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
Extracts buildQrUrl and generateZoneQrs with injectable I/O deps and adds
tests/scripts/generate-qr.test.ts (20 tests). Refactor + tests originated
from a concurrent working session; this commit adds on top:
- import.meta entry guard — the unconditional top-level main() executed on
test import and wrote (sometimes truncated) PNGs into public/qr
- typed vi.fn generics in makeDeps so strict tsc passes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
- supabase/config.toml + supabase/.gitignore from supabase init (needed
for supabase db push / local dev)
- phase 2 and phase 4 implementation plans referenced by the SDD
progress ledger but never committed
- ignore local tooling dirs (node_modules.nosync, graphify-out, .claude)
and iCloud "name 2.ext" sync-conflict copies
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
- fetch URL: /api/incidents → /ims/api/incidents (basePath not auto-prepended in client fetch)
- replace syncing state guard with syncingRef to give syncNow a stable reference, preventing useEffect from re-registering the online listener on every sync cycle
- keep syncing state for UI rendering only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
- Add new migration 20260711000013_match_incidents_auth_guard.sql that
replaces match_incidents with an inline auth guard: callers without
hse/admin role receive PGRST301 Forbidden, closing the SECURITY
DEFINER RLS bypass.
- Wrap anthropic.messages.create() in try/catch returning 503 in all
four AI routes: quality-check, triage-suggest, rca-draft, similar.
- Wrap JSON.parse(inc.embedding) and embedText() in similar/route.ts
in a shared try/catch returning 503 Embedding service unavailable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr