Commit Graph
100 Commits
Author SHA1 Message Date
admin 3fd8dfe294 fix(pdf): sanitise non-WinAnsi chars, handle newlines in wrap 2026-07-28 17:53:04 +08:00
admin 740c6da3ba fix(pdf): sanitise Content-Disposition filename, fix triagedByName test fixture 2026-07-28 17:11:16 +08:00
admin 28929f40e2 feat(ui): add Print Report button on closed incidents 2026-07-28 17:06:33 +08:00
adminandClaude Sonnet 4.6 1b0941ec69 feat(pdf): add incident report PDF API route
GET /api/incidents/[id]/report-pdf — hse/admin only, closed incidents only.
Joins incidents + sites + zones + users + investigations + capaActions + addenda,
then delegates to buildIncidentReportPdf() and returns application/pdf download.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HEYxFQiCyxJvnBCoZeYzB9
2026-07-28 17:03:00 +08:00
adminandClaude Sonnet 4.6 e7d82f3a5a fix(pdf): skip section gap on new page, reserve space for CAPA label
- Fix 1: drawSection now checks if ensureSpace triggered a page break by
  comparing state.y before/after. Only subtract SECTION_GAP if we're not
  at the top of a fresh page, avoiding wasting 20px after page breaks.

- Fix 2: CAPA loop now reserves LINE_HEIGHT * 4 instead of just LINE_HEIGHT
  to keep the label and at least a few fields together on the same page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HEYxFQiCyxJvnBCoZeYzB9
2026-07-28 16:56:13 +08:00
admin 8bbdd4f843 feat(pdf): add incident report PDF builder 2026-07-28 16:52:31 +08:00
admin dbc66460c4 fix(dashboard): include all CAPAs in on-time rate, count open+not-overdue as on-time 2026-07-28 16:36:57 +08:00
admin 87251b3fc6 fix(dashboard): compute CAPA overdue count from due_date instead of status field 2026-07-28 16:31:45 +08:00
admin 1103d842d5 fix(deploy): use sudo for systemctl restart ims 2026-07-28 16:26:58 +08:00
admin 6b427b840d fix(capa): compute overdue status at render time in CAPA board page 2026-07-28 16:21:23 +08:00
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
adminandClaude Sonnet 4.6 8acca41ca1 docs: phase 9 — update vps-cron.md, add cutover runbook
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 06:35:15 +08:00
adminandClaude Sonnet 4.6 b3811b3633 feat(ops): phase 8 — data migration scripts + fix password_reset_tokens in schema.sql
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 06:30:34 +08:00
admin 04c36f9d45 fix(sw): remove dead pathname startsWith('/') guard after basePath drop 2026-07-23 22:34:20 +08:00
adminandClaude Sonnet 4.6 0389d6091a fix(config): phase 7 follow-up — fix remaining /ims/ occurrences in sw.js, notifications, components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 22:32:11 +08:00
adminandClaude Sonnet 4.6 9d047e168f feat(config): phase 7 — drop /ims basePath, update all hardcoded paths, refresh env spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 22:27:46 +08:00
adminandClaude Sonnet 4.6 d785d86636 feat(email): phase 6 — replace Resend with Brevo transactional email
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>
2026-07-23 22:20:12 +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 dacc1a5265 fix(db): phase 4 group 6 — session guard, type safety, SQL filter fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 17:33:48 +08:00
adminandClaude Sonnet 4.6 f591c0be18 feat(db): phase 4 group 6 — server component pages to Drizzle
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>
2026-07-23 17:29:22 +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 a95273b182 feat(db): Drizzle DAL with withUser/asAdmin GUC wrapper (Phase 2)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 15:50:57 +08:00
adminandClaude Sonnet 4.6 e5fd2436fa feat(db): consolidated PostgreSQL schema replacing Supabase (Phase 1)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 15:43:09 +08:00
adminandClaude Sonnet 4.6 ba3d345531 feat(ops): add new server runbook and update deploy.sh for ims.setia.com.my
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 15:30:51 +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
admin 938eb7e08d feat(capa): wire owner notes form into capa-owner page and HSE detail view 2026-07-23 12:00:43 +08:00
admin 5f34dbb644 feat(capa): add CapaOwnerNotesForm component 2026-07-23 11:57:43 +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
admin 52171ac3ca feat(db): add owner_notes column to capa_actions 2026-07-23 11:53:41 +08:00
adminandClaude Sonnet 4.6 71cf90e1de feat(capa): add Close CAPA button for HSE after verification
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>
2026-07-23 11:41:56 +08:00
adminandClaude Sonnet 4.6 1a813d54f2 fix(middleware): exclude /api/ routes from role-prefix guard
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
2026-07-22 20:00:27 +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
adminandClaude Sonnet 4.6 4fbab33de4 fix(auth): add forgot/reset to public routes, use Link for basePath, add session guard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WMymkhHZiaYZtUeH9MEHZQ
2026-07-21 22:36:53 +08:00
admin fa88d62375 feat(auth): add reset-password page and login success banner 2026-07-21 22:31:20 +08:00
admin 211117e61a feat(auth): add forgot-password page 2026-07-21 22:29:39 +08:00
admin 4d7ab5d7da feat(auth): add forgot password link to login form 2026-07-21 22:28:26 +08:00
admin 3a5daaa987 docs: add forgot password implementation plan 2026-07-21 22:27:02 +08:00
admin a4b7761f92 docs: add forgot password flow design spec 2026-07-21 22:25:18 +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
admin ac2f0724dd fix: add urine_test_result to supervisor incident detail select 2026-07-17 19:11:11 +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
admin e687510d0b fix(capa): show all active users in Assigned To dropdown 2026-07-17 19:05:37 +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 370b985375 feat(admin): truck report link + pre-fill form via ?truck_id=
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
2026-07-13 11:49:56 +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 6c1ad71643 feat: validate transport incident requires truck_id
Add 'transport' to INCIDENT_TYPES; add truck_id field to IncidentInput;
reject transport submissions missing truck_id.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
2026-07-13 10:50:13 +08:00
adminandClaude Sonnet 4.6 ebeab27e5e feat(db): trucks table, transport enum value, incidents.truck_id FK
- 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
2026-07-13 10:49:06 +08:00
adminandClaude Sonnet 4.6 5416929cab feat: show investigation panel on incident detail page
Fetches investigation record (5-Why steps, findings, root cause,
alcohol test, witness refs) and renders below incident details.
Shows completion status and investigator name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
2026-07-13 10:15:08 +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 e9fd29035a fix: allow admin role to access triage and investigation pages
Role guard was hse-only — admin was redirected to inbox on click.
Both triage and investigation now accept hse or admin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
2026-07-13 10:00:17 +08:00
adminandClaude Sonnet 4.6 8a9416abb9 fix: lower sidebar breakpoint from md to sm so desktop sidebar renders
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
2026-07-13 09:57:29 +08:00
adminandClaude Sonnet 4.6 b1730dbeb1 style: darken form-field text and placeholders globally
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
2026-07-13 07:19:51 +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 614c792225 fix(security): VULN-009 magic-byte MIME validation + UTC date fix
- 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
2026-07-13 06:37:59 +08:00
admin 3776bc20b3 fix: rename Anthropic→DeepSeek label in settings, darken text colors 2026-07-12 22:35:02 +08:00
admin a072ec0a3e fix: add Settings link to admin sidebar nav 2026-07-12 22:27:00 +08:00
adminandClaude Sonnet 4.6 8061f804f7 feat: self-service change password for all roles
- 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
2026-07-12 22:17:51 +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 dba79c0f17 fix: drop-if-exists before recreating notifications RLS policies 2026-07-12 17:34:44 +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
admin c0ec6660ef feat: sticky incident header with top-positioned action buttons (fix #5) 2026-07-12 16:44:42 +08:00
admin f6064b9580 feat: reporter clickable rows, CAPA owner action buttons, triage segmented severity 2026-07-12 16:42:49 +08:00
admin 180b0dc0a3 feat: persistent sidebar nav with role-scoped links, logout, and mobile bottom tab bar 2026-07-12 16:40:57 +08:00
admin ba2cf69a8b feat: add cross-section nav links on admin page for admin role 2026-07-12 16:24:05 +08:00
admin 580a60bbd8 fix: admin role bypasses path prefix guard — can access all protected routes 2026-07-12 15:03:56 +08:00
admin 706a075d73 fix: use nextUrl.clone() for middleware redirects to preserve basePath /ims 2026-07-12 14:39:01 +08:00
admin c1c874b3fe fix: exclude /api/cron from auth middleware so cron jobs run unauthenticated 2026-07-12 13:59:54 +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 e2559d1f59 docs: real README, current phase status in CLAUDE.md, complete env example
- 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
2026-07-12 10:46:44 +08:00
adminandClaude Fable 5 81282fbd8a refactor: make generate-qr testable with injectable deps + entry guard
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
2026-07-12 10:40:38 +08:00
adminandClaude Fable 5 646c94be0c chore: commit supabase config, phase 2/4 plan docs, extend gitignore
- 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
2026-07-12 10:30:06 +08:00
adminandClaude Fable 5 f1d426adf1 chore: add idb dependency missed in Phase 4 offline-capture commit
lib/offline/db.ts has imported idb since c048878 but the dependency
was never committed to package.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPf5Rc8QPx6V8KLEEgfKEQ
2026-07-12 10:25:21 +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
admin 98c38c3716 fix: use setUTCDate in getNextRecheckDate to avoid local-timezone off-by-one 2026-07-11 21:49:43 +08:00
adminandClaude Sonnet 4.6 e4d1da033d fix: prefix all client-side fetch calls with /ims basePath
Client components were calling fetch('/api/...') without the /ims prefix,
causing 404s in production where Next.js serves under basePath: '/ims'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
2026-07-11 21:26:18 +08:00
adminandClaude Sonnet 4.6 448ea4857a fix: offline sync basePath prefix and stable syncNow callback
- 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
2026-07-11 19:11:16 +08:00
admin c048878600 feat: PWA offline capture — IndexedDB queue, service worker cache, auto-sync on reconnect 2026-07-11 19:07:35 +08:00
admin 8c88118b15 chore: update progress ledger — Phase 3 v2 review complete 2026-07-11 19:04:20 +08:00
admin 871ef7c53a fix: rename on-time rate label, hoist getApiKey above escalation loop, remove broken capa-owner incident link 2026-07-11 19:03:23 +08:00
adminandClaude Sonnet 4.6 87264a5497 feat: i18n — EN/MS/ZH translations with cookie-based locale switching, report form translated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
2026-07-11 19:01:44 +08:00
admin c67c489ab3 chore: mark Phase 3 complete in progress ledger 2026-07-11 18:56:10 +08:00