Commit Graph
59 Commits
Author SHA1 Message Date
admin 46d82df377 fix(capa): advance incident status to capa_pending on CAPA creation 2026-07-28 15:54:47 +08:00
adminandClaude ed2a2f65e4 fix: switch AI endpoints from tool-calling to JSON output mode
DeepSeek v4-pro reasoning model rejects tool_choice parameter.
All 4 endpoints now use system prompts with JSON schema
instructions and parse content as JSON instead of tool_calls.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-26 10:49:44 +08:00
adminandClaude 7bf3b6a409 fix: rename deepseek model from deepseek-chat to deepseek-v4-pro
DeepSeek API deprecated deepseek-chat model. Returns error:
'the supported API model names are deepseek-v4-pro or deepseek-v4-flash'

Affected all 4 AI endpoints:
- risk-flags (90-day dashboard)
- triage-suggest
- rca-draft
- quality-check

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-26 10:17:26 +08:00
admin 6a2551546d fix: log DeepSeek error in risk-flags catch block 2026-07-25 22:08:18 +08:00
adminandClaude Sonnet 4.6 9c67014bb9 fix(audit): wrap admin + capa mutations with their audit writes in single transactions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 06:44:05 +08:00
admin 186c732ac0 fix(storage): add X-Content-Type-Options: nosniff to evidence serve route 2026-07-23 22:16:47 +08:00
adminandClaude Sonnet 4.6 e5f74c367d feat(storage): phase 5 — replace Supabase storage with local filesystem + HMAC evidence serving
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 22:14:54 +08:00
adminandClaude Sonnet 4.6 c2db693d9f feat(db): phase 4 group 5 — dashboard/reports/settings/notifications/users routes to Drizzle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 17:13:51 +08:00
adminandClaude Sonnet 4.6 853675118d feat(db): phase 4 group 4 — CAPA routes to Drizzle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 17:05:58 +08:00
adminandClaude Sonnet 4.6 98f5c4e421 feat(db): phase 4 group 3 — incident routes to Drizzle
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>
2026-07-23 16:57:01 +08:00
adminandClaude Sonnet 4.6 d234ebf916 feat(db): phase 4 group 2 — admin routes to Drizzle
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>
2026-07-23 16:44:39 +08:00
adminandClaude Sonnet 4.6 25f923f530 feat(db): phase 4 group 1 — lib/ settings + notifications to Drizzle
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>
2026-07-23 16:37:51 +08:00
adminandClaude Sonnet 4.6 d18d29168a feat(auth): phase 3 — replace Supabase GoTrue with bcryptjs+jose
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>
2026-07-23 16:20:04 +08:00
adminandClaude Sonnet 4.6 509ed90fe7 fix(capa): field-level auth split, status gate, RLS tightening, audit old_value, hide form on pending_verification
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 12:10:53 +08:00
adminandClaude Sonnet 4.6 96eaeb7aef feat(api): expose owner_notes in capa GET and PATCH
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>
2026-07-23 11:56:10 +08:00
adminandClaude Sonnet 4.6 0e479b648f fix(auth,capa): restore auth callback, fix CAPA status update
- 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
2026-07-22 19:43:38 +08:00
admin d5803daa67 fix(admin): delete auth user on profile update failure 2026-07-18 18:00:00 +08:00
adminandClaude Sonnet 4.6 33d4dd4b27 feat(admin): remove invite-by-email, direct-add only
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
2026-07-18 17:48:26 +08:00
adminandClaude Sonnet 4.6 e30d583947 feat: split alcohol and urine test into separate fields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-17 19:08:27 +08:00
adminandClaude Sonnet 4.6 a3ce59f2a8 fix(auth): return 503 when NEXT_PUBLIC_APP_URL unset for invite redirectTo
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>
2026-07-17 19:03:58 +08:00
adminandClaude Sonnet 4.6 e682162bbc fix(auth): redirect to appUrl after invite callback
origin lacks /ims basePath; use NEXT_PUBLIC_APP_URL instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzvzV91UqHZKM9P58qtvrA
2026-07-16 21:44:12 +08:00
adminandClaude Sonnet 4.6 b08fec385d fix(capa): filter owner dropdown by capa_owner role
Was filtering by non-null department, excluding capa_owner
users who had no department set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BzvzV91UqHZKM9P58qtvrA
2026-07-16 21:12:12 +08:00
adminandClaude Sonnet 4.6 e949016bd4 feat(admin): add users directly with admin-set password + phone field
- POST /api/admin/users: if password supplied → createUser (instant, email_confirm:true); else inviteUserByEmail (existing flow)
- password validated ≥8 chars; never logged in audit_log
- profile update now also saves phone number
- UI: mode toggle (Invite by email / Add directly), phone input always visible, password input shown only in 'Add directly' mode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
2026-07-13 16:36:20 +08:00
adminandClaude Sonnet 4.6 690485f74e feat: delete + deactivate for sites, zones, and trucks; harden incident API error handling
- zones: add active column (migration 20260713000005)
- sites/zones API: PATCH (toggle active) + DELETE (blocked when incidents reference it)
- trucks API: PATCH + DELETE with same pattern
- admin page: select active for sites + zones
- site-zone-manager + truck-manager: deactivate toggle + delete button per row with busyId
- incidents API: reject reports on deactivated zone/site; wrap handler in top-level try-catch so unhandled errors return JSON (not HTML)
- report-form: parse JSON separately so HTTP status code surfaces instead of generic "Something went wrong"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
2026-07-13 16:05:36 +08:00
adminandClaude Sonnet 4.6 f90bf4ed03 feat: transport incidents with truck number support
- DB: trucks table + incidents.truck_id FK + transport enum value
- Validation: transport type requires truck_id
- Create API: validates truck exists/active, persists truck_id
- Report form: truck dropdown shown when type=transport (required)
- Admin: TruckManager CRUD + /api/admin/trucks route
- Detail: trucks join surfaced in incident-detail + detail page query
- Inbox (HSE + supervisor): truck filter, transport in TYPE_OPTIONS,
  fixed stale enum values (dropped dangerous_occurrence/mhe_asset/occupational_disease)
- List: transport label + truck number badge in rows
- i18n: transport + truckLabel/truckPlaceholder in en/ms/zh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
2026-07-13 10:56:07 +08:00
adminandClaude Sonnet 4.6 782a9687e2 fix: allow admin role to submit triage and investigation API routes
API routes were hse-only, blocking admin from saving triage/investigation
forms even after the page-level guards were fixed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
2026-07-13 10:04:32 +08:00
adminandClaude Sonnet 4.6 c00879bf66 fix: recover orphaned DeepSeek key + remove retired settings slots
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
2026-07-13 07:11:09 +08:00
adminandClaude Sonnet 4.6 d10c690c12 feat: switch embeddings from Voyage AI to Google Gemini text-embedding-004
- 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
2026-07-13 06:50:35 +08:00
adminandClaude Sonnet 4.6 b2891a433d feat: switch AI provider from Anthropic to DeepSeek
- 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
2026-07-12 21:51:15 +08:00
adminandClaude Sonnet 4.6 8fe036bc1a fix: P2 API hardening — unbounded SELECTs, export audit log, header injection, empty-key guard
- export: add .limit(10000), sanitize filename, write_audit_log on every export
- stats: add .limit(10000) to aggregation query
- settings POST: reject empty string values to prevent silent key deletion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
2026-07-12 21:25:35 +08:00
adminandClaude Sonnet 4.6 16dd62df11 fix: P1 API security hardening — rate limits, auth guards, duplicate prevention
- verify/route.ts: setDate → setUTCDate to avoid timezone off-by-one on recheck date
- triage-suggest, rca-draft, quality-check: 60s per-user rate limit via audit_log
- quality-check: add write_audit_log (was missing, CLAUDE.md violation)
- investigation POST: 409 if investigation already exists for incident
- incidents POST: 60s per-user rate limit via audit_log
- addenda GET: restrict to hse/admin/supervisor roles
- dashboard/stats GET: restrict to hse/admin/management roles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
2026-07-12 20:47:32 +08:00
admin 1879def32c security: P0 fixes — IDOR on incident/CAPA, CAPA non-owner write, timing-safe recheck cron, auth callback open redirect 2026-07-12 20:36:28 +08:00
admin c80091c8d6 security: CAPA privilege escalation, CSV injection, AI rate-limit, prompt injection guard, open redirect, timing-safe cron secret, server-only admin client, notifications RLS 2026-07-12 17:33:58 +08:00
adminandClaude Fable 5 4242af029c fix: code-review findings — closed-incident embedding 503, hardening, dedup
- /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
2026-07-12 12:45:01 +08:00
adminandClaude Fable 5 576557181a feat: Phase 5 & 6 — usability, compliance hardening, analytics
Phase 5 (usability + compliance):
- In-app notification bell/badge: migration 016 adds read state + per-user
  RLS + create_in_app_notification SECURITY DEFINER RPC; /api/notifications;
  wired into incident creation, CAPA assign/verify, escalation cron
- Incident closure: new POST /api/incidents/[id]/close (requires verification
  status + all CAPAs verified); migration 017 locks closed incidents at DB
  level (update/delete triggers) with append-only incident_addenda + UI panel
- Server-side pagination on HSE/supervisor incident inboxes (.range, 25/page)
- Investigation form: alcohol/urine test result + witness statement refs
  (existing schema columns, now editable)
- Type-specific intake fields: migration 018 adds incidents.type_details
  JSONB; whitelist validation; environmental/asset/security/fire field
  groups in report form; EN/MS/ZH labels; offline queue support
- JKKP 8 annual register CSV export (/api/reports/jkkp8) + dashboard button
  + January statutory deadline banner
- Admin page: user invite (service-role client), role/site/active management,
  site + zone CRUD with QR report links — replaces Phase 0 stub
- Evidence gallery thumbnails via Supabase render transform with fallback

Phase 6 (analytics):
- 12-month stacked trend chart (leading/lagging/other) + top root causes
  (lib/dashboard/trends.ts pure helpers)
- AI rising-risk zones: /api/dashboard/ai/risk-flags aggregates 90-day
  zone stats, claude-opus-4-8 forced tool_use, panel on HSE + management
  dashboards, suggestion audit-logged

Also fixes 9 pre-existing missing /ims basePath prefixes in client fetches
and download links.

132 tests passing, tsc clean, next build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
2026-07-12 10:25:08 +08:00
adminandClaude Sonnet 4.6 b9def9c957 feat: CAPA effectiveness re-check cron — 30/60/90-day email + WhatsApp notifications
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
2026-07-11 18:55:27 +08:00
adminandClaude Sonnet 4.6 a47f3aabc9 feat: WhatsApp notifications — new incident alert and CAPA overdue escalation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
2026-07-11 18:50:25 +08:00
admin 121433ddf8 Revert "feat: WhatsApp notification helper + phase 4 migration (recheck_round, WhatsApp settings)"
This reverts commit b6845f5bb5.
2026-07-11 18:47:08 +08:00
admin dfca872629 feat: CSV export endpoint for HSE and management dashboards 2026-07-11 18:44:52 +08:00
admin b6845f5bb5 feat: WhatsApp notification helper + phase 4 migration (recheck_round, WhatsApp settings) 2026-07-11 18:43:57 +08:00
adminandClaude Sonnet 4.6 b9ab94c9da feat: API key settings page — store ANTHROPIC/VOYAGE keys in DB with admin UI
- Migration: app_settings table with admin-only RLS (ANTHROPIC_API_KEY, VOYAGE_API_KEY)
- lib/settings.ts: getApiKey() reads DB first, falls back to env var
- lib/claude/client.ts: factory createAnthropicClient(apiKey) replaces singleton
- lib/claude/embed.ts: optional apiKey param, falls back to env
- 3 Claude AI routes + similar route: fetch key from settings before calling AI
- incidents/route.ts: fire-and-forget embed reads VOYAGE key from settings
- GET/POST /api/settings: admin-only masked key management endpoint
- /hse/settings page + ApiKeyForm client component

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
2026-07-11 17:46:40 +08:00
adminandClaude Sonnet 4.6 c07bdb77ad fix: RLS guard in match_incidents + try/catch around AI/embed calls
- 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
2026-07-11 16:48:20 +08:00
adminandClaude Sonnet 4.6 10e71969dd feat: AI RCA draft assistant in investigation form
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
2026-07-11 16:35:12 +08:00
adminandClaude Sonnet 4.6 547cc6e817 feat: pgvector similar-incident retrieval + SimilarIncidentsPanel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
2026-07-11 16:31:02 +08:00
adminandClaude Sonnet 4.6 f10fa071f9 feat: AI triage suggestion — severity + DOSH flags pre-fill
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
2026-07-11 16:27:29 +08:00
admin a5933d2cd9 fix: guard JSON parse and validate Claude tool output in quality-check route 2026-07-11 16:23:31 +08:00
adminandClaude Sonnet 4.6 3bec95aaf8 feat: AI report quality check before submission
Adds POST /api/incidents/ai/quality-check using Claude tool_use to score incident descriptions 1-10. Report form blocks submission when score < 6, shows amber feedback with suggestions, and offers a 'Submit anyway' override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
2026-07-11 16:18:39 +08:00
adminandClaude Opus 4.8 54332d0297 feat: JKKP 6/7 PDF generation via pdf-lib, download from incident detail
Also excludes node_modules.nosync from vitest test discovery to fix pre-existing bleed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
2026-07-11 15:24:18 +08:00
adminandClaude Opus 4.8 75fa2605bf feat: CAPA overdue escalation cron — 4 thresholds, once-per-threshold via notifications_log
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
2026-07-11 15:20:15 +08:00
adminandClaude Opus 4.8 804b1fbf33 feat: verification flow — HSE verify/reopen CAPA, auto-transition incident to verification
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
2026-07-11 15:18:17 +08:00