Site Incident Reporter

Product & Architecture Briefing — Pete + Jerry
Draft — 7 open questions
Date 22 April 2026
Pete (Architecture) ✓ Complete
Jerry (Product) ✓ Complete
Stack Next.js + Supabase + Vercel
0/6 reviewed
📋 What We're Building

A QR-code-triggered incident reporting progressive web app for workplace and site environments. Workers scan a QR code posted at a physical location, fill in (or speak) a short description of the incident, select severity, and submit — all in under 60 seconds, with no account, no app install, and no friction.

Behind the simple reporter screen: AI analysis classifies the submission, flags urgent events for immediate admin notification, and feeds a management dashboard showing trends by location over time.

👥 Three User Types
📱 Reporter

Worker or visitor on-site. Stressed, in a hurry, standing up, using a phone. Needs a 3-tap flow. No login ever.

🔔 Admin

Site safety manager. Receives urgent alerts. Manages locations, QR codes, incident status, and internal notes.

📊 Management

Senior leadership. Views aggregate trends — incidents per location/month, severity breakdowns. Not involved in individual cases.

🔄 How It Works — End to End
[Physical Location] └── QR code posted on wall / machinery / vehicle │ encodes: https://app.domain.com/report/[locationId][Reporter's Phone] └── Mobile browser opens — location pre-loaded └── Fills notes + selects severity OR uses voice-to-text └── Taps Submit → confirmation screen in <2 seconds │ ▼ [Next.js API on Vercel] └── Validates locationId → INSERT incident to Supabase └── Returns 200 immediately ← reporter never waits for AI │ ▼ [Supabase Edge Function — async] └── Calls OpenAI GPT-4o with notes text └── Receives structured JSON analysis └── UPDATE incident record with ai_result └── IF escalate = true → triggers notification │ ▼ [Notification Layer] └── Email via Resend (all urgent incidents) └── SMS via Twilio (Emergency severity) └── Realtime push → Admin dashboard toast │ ▼ [Admin Dashboard + Management Reporting] └── Incident list with AI analysis panel └── Trend charts, location heatmaps, severity breakdown
🎯 MVP Scope (v1)
IN v1
QR code reporter flow
Location-aware, no login, voice or text input
AI analysis on submission
Severity, injury risk, category, summary sentence
Admin alerts (email + push)
Urgent incidents notify admin in <10 seconds
Admin dashboard
Incident list, detail view, status management, location & QR management
Management reporting
Trend charts by location, severity breakdown, KPI summary
PHASE 2
Escalation chains
If admin doesn't acknowledge within 30 min → secondary contact
PDF board reports
Scheduled monthly summary exports
Photo attachments
Reporter can attach a photo on submission
Multi-language reporter screen
For multilingual workforces
HSEQ system integrations
Vault, Safety Champion, etc.
🔄 End-to-End Workflow — AI Actions & Human Gates
PHYSICAL / QR REPORTER SYSTEM + AI ADMIN / HUMAN QR Code location URL encoded Mobile Browser no install, no login Report Form type or speak + severity Submit POST /api/incidents Confirmed ✓ <2 sec, ref number shown Store to DB Supabase INSERT Edge Function async, DB webhook trigger GPT-4o Analysis structured JSON output Update Incident ai_result JSONB written Escalate? check AI flags Log Only minor — no notification NO YES 🔔 Admin Alerted push + email (<10 sec) 👤 Admin Reviews reads notes + AI panel ⚡ Admin Acts status → In Progress 📊 Trend Data management dashboard System / Data Reporter / Human action AI (async) Decision gate Async / background Escalation path 🔷 Human gate 🔮 AI action
📱 Reporter Journey — "I need to report something right now"
💡
Design constraint

The reporter is standing up, on a phone, possibly stressed or in poor light. Every design decision is filtered through this reality. Speed and clarity over completeness.

Screen 1 — Report Form
Report an Incident
📍 Level 3 — Warehouse B
What happened?
Describe the incident…
🎙 Speak instead
How serious is it?
Minor
Serious
Emergency
Submit Report
Screen 2 — Voice Input Active
Report an Incident
📍 Level 3 — Warehouse B
Listening…
🎙
Transcript
Worker slipped near the loading dock. Wet floor, no sign posted. She hit her knee on the concrete.
Done speaking
Screen 3 — Ready to Submit
Report an Incident
📍 Level 3 — Warehouse B
What happened?
Worker slipped near the loading dock. Wet floor, no sign posted. She hit her knee on the concrete.
How serious is it?
Minor
Serious
Emergency
Submit Report
Screen 4 — Confirmation
Report Submitted
Reference: WH3-4F2
Your report has been submitted. A safety manager has been notified.
Report another incident
1
Scan QR code

Worker scans QR code posted at the location. Mobile browser opens directly — no app install, no splash screen.

2
Location confirmation

Location name shown prominently ("Level 3 — Warehouse B") so reporter confirms they're on the right screen. One screen only — notes field + voice button + three severity buttons.

3
Enter details — type or speak

Large text area + microphone button labeled "Speak instead". Voice activates browser-native speech-to-text, words appear in real time. Reporter taps "Done speaking" when finished.

4
Select severity

Three large pill buttons: Minor / Serious / Emergency. No default — reporter must make a deliberate choice. Minimum 48px height for fat fingers.

5
Submit + confirmation

Full-width submit button. Target: <2 seconds. Confirmation screen shows a checkmark, reference number, and severity-appropriate message. Form does not disappear on network error — content is preserved.

Confirmation messages by severity
Minor "Your report has been submitted and logged."
Serious "Your report has been submitted. A safety manager has been notified."
Emergency "This has been flagged as an emergency. A manager is being notified now."
🔔 Admin Journey — "Something happened and I need to act"
A
Receive alert

Push notification (mobile) or email arrives with location, severity badge, first 100 characters of notes, and a deep link to the full report.

B
Review incident

Opens to incident detail: location, time, severity badge, full notes, AI analysis panel (collapsed, expandable), status field. Minimal scrolling — key info above the fold.

C
Take action

Status change (Open → In Progress → Closed) with one tap + confirm. Internal notes field — not visible to reporter. Admin is probably standing on-site on their phone.

D
Manage locations

Location list shows: name, incidents this month, last incident date, QR status. Create/edit location → "Download QR code" generates a print-ready PNG. Admin prints and laminates it.

Admin — Push Notification
🚨
Site Incident Reporter
now
[URGENT] Incident at Level 3 — Warehouse B
Serious — Worker slipped near the loading dock. Wet floor, no sign posted…
View full report →
Swipe to dismiss · Tap to open
Admin — Incident Detail
← Back
Incident WH3-4F2
22 Apr 2026, 14:33
Serious
📍 Level 3 — Warehouse B
Worker slipped near the loading dock. Wet floor, no sign posted. She hit her knee on the concrete.
🤖 AI Analysis
Category: Slip / Trip / Fall
Injury risk: Likely
Persons affected: One
Summary: Worker injured knee after slipping on unmarked wet floor.
STATUS
Open
In Progress
Closed
Management — Dashboard
Safety Dashboard
April 2026 · All locations
Total Incidents
24
▼ 3 vs last month
Emergencies
2
▲ 1 vs last month
Incidents by week
By location
Warehouse B
9
Loading Dock
6
Car Park
4
📊 Management Dashboard — "How are we tracking on safety?"
Module 1 — Summary KPIs

Four tiles: total incidents this period, vs prior period (delta + colour), emergency incidents (red if >0), locations with zero incidents (green — positive signal).

Module 2 — Volume Over Time

Bar chart, one bar per week, stacked by severity. Date range selector: this month / last 3 months / this year / custom.

Module 3 — By Location

Horizontal bar chart ranked by volume. Colour indicates most serious incident at that location. Tap a row to filter the full dashboard to that location.

Module 4 — Category Breakdown

Donut chart of AI-assigned incident categories. Tap a segment to see count and percentage.

Module 5 — Severity Trend

Three-line chart: Minor, Serious, Emergency counts over time. Answers the question: "Are we getting safer?" independently of total volume.

What management cannot see

Individual incident notes, reporter identity (if captured), or free text. Aggregate data only. Management views are separate from admin views — different permission level.

🏗️ Stack Decision
LayerTechnologyRationale
FrameworkNext.js 16 (App Router) + TypeScriptApp Router enables server components for fast dashboard queries; established standard across all projects
Database + AuthSupabase (Postgres + RLS + Auth)Managed Postgres + RLS, no auth to hand-roll, Edge Functions for async AI pipeline, Realtime for push
DeploymentVercelZero-config CI/CD, auto-preview per PR, global CDN — no servers to manage
AI analysisOpenAI GPT-4o via Supabase Edge FunctionStructured output mode; <$0.01/analysis; async so reporter never waits
Speech-to-textWeb Speech API (browser-native)Free, real-time, no round-trip, no API key. Upgrade path to OpenAI Whisper if needed.
Email notificationsResendClean API, Next.js-native DX, generous free tier
SMS notificationsTwilio (Emergency severity only)Reliable delivery fallback when admin may not have push enabled
ChartsRecharts or TremorLightweight, React-native charting without a heavy BI dependency
🔗 QR Code Strategy
URL-encoded approach — no decoding logic needed

The QR code encodes the full HTTPS URL directly. The location ID is the URL.

https://app.domain.com/report/[locationId]
  • Generation: Admin dashboard generates print-ready PNG on demand using the qrcode npm library. Location name printed below the code.
  • Validation: On page load, [locationId] is validated against Supabase via a server component. Invalid IDs render a graceful error page — not a broken form.
  • Vanity slugs: A short alphanumeric slug can be stored alongside the UUID as an alternate key if cleaner URLs are wanted later. Low effort, no urgency.
  • Physical security note: A malicious actor could replace a printed QR with one pointing to a phishing page. Recommend laminated/tamper-proof QR sheets. Software cannot solve this.
🎙️ Speech-to-Text
Recommended: Web Speech API
  • Cost: Free (browser handles it)
  • Latency: Real-time, no upload round-trip
  • Privacy: Audio processed by browser vendor, not sent to our servers
  • Support: Chrome (Android), Safari 14.1+ (iOS), Edge
  • Fallback: Unsupported browsers show: "Voice not available — please type your report."
Upgrade path: OpenAI Whisper

Phase 2 only if needed

  • Better accuracy for accented speech and noisy environments
  • Requires audio capture, upload, and API key management
  • Per-minute cost
  • This decision does not need to be made now — the architecture supports both
📁 App Structure
pwa/
├── app/
│   ├── report/[locationId]/       # Public reporter form — no auth
│   ├── (admin)/
│   │   ├── dashboard/             # Incident list + status management
│   │   ├── incidents/[id]/        # Incident detail + AI panel
│   │   ├── locations/             # Location management + QR generation
│   │   └── reporting/             # Management trend charts
│   └── api/
│       ├── incidents/route.ts     # POST — accepts submissions
│       └── locations/[id]/route.ts
├── lib/
│   ├── supabase.ts                # Client (anon key, RLS enforced)
│   └── supabase-admin.ts          # Server-only (service role)
├── components/
│   ├── reporter/                  # SpeechInput, SeverityPicker, SubmitForm
│   ├── dashboard/                 # IncidentList, IncidentDetail, AIPanel
│   └── reporting/                 # Charts, KPI tiles
└── supabase/
    ├── functions/
    │   ├── analyse-incident/      # Edge Function — AI + notification trigger
    └── migrations/
        └── 001_initial_schema.sql
🤖 AI Analysis — What Gets Extracted
ℹ️
Advisory only

The AI analysis does not override the reporter's severity selection. It surfaces risk signals a busy admin might miss when reading quickly. It does not make safety decisions.

FieldTypePurpose
severity_assessmentlow / medium / high / criticalAI's independent read of the notes. Flagged in UI if it contradicts reporter's choice — a safety net for under-reporting.
injury_risknone / possible / likely / confirmed"Likely" or "confirmed" triggers immediate admin notification regardless of reporter severity.
incident_categoryenumPrimary category for trend reporting. Values: slip-trip-fall, equipment-failure, chemical-exposure, vehicle, violence, fire, near-miss, illness, environmental, other.
persons_affectednone / one / multiple / unknownExtracted from language patterns in the notes.
urgency_signalbooleanTrue if notes describe active danger (fire still burning, gas still leaking). Triggers notification regardless of reporter's severity selection.
summary_sentencestring (120 chars max)Plain-English summary used in notification previews. Admin understands situation without opening the app.
confidence_scorefloat 0–1Below 0.5: admin sees caveat "Low confidence — read the full notes carefully."
recommended_responsestring, nullableIf category implies a known protocol (chemical spill, gas leak), surface a plain-language prompt. Advisory only.
⚠️
What the AI must never do

Make any determination about organisational liability. Speculate about causes not in the notes. Recommend dismissing or downgrading an incident. Extract or store personal identifying information from notes text.

AI Pipeline — Async After Submission
Reporter submits form
  ↓
POST /api/incidents
  ↓
INSERT to incidents table  →  200 OK returned to reporter immediately
  ↓ (async, does not block reporter)
Supabase DB Webhook fires → Edge Function: analyse-incident
  ↓
Call OpenAI GPT-4o (structured output mode)
  ↓
UPDATE incidents SET ai_result = {...}
  ↓
IF escalate = true
  → Email via Resend
  → SMS via Twilio (Emergency only)
  → Realtime push → Admin dashboard toast
On AI failure

Incident is stored without analysis. Admin sees "Analysis pending". A retry job attempts analysis within 5 minutes. Failed jobs logged in a failed_ai_jobs table for monitoring.

Cost at scale

GPT-4o at <$0.01 per incident. For 1,000 incidents/month = ~$10/month in AI costs. Negligible for any commercial deployment.

🚨 Notification Triggers
A notification is sent to admin when ANY of the following are true:
🔴 Reporter selected Emergency severity
🟡 Reporter selected Serious AND AI injury_risk is "likely" or "confirmed"
AI urgency_signal = true (active danger) — regardless of reporter's severity selection
🤖 AI severity_assessment = "critical" — regardless of reporter's selection

The AI acts as a safety net for under-reporting. Minor submissions that trigger none of the above are logged silently — they appear in the admin incident list but generate no notification.

📧 Notification Content
Push notification (mobile)
[URGENT] Incident at Level 3 — Warehouse B
Emergency — Worker reported slip near loading bay, injury possible.
Submitted 3 minutes ago
View full report →
Email (always sent for Emergency; fallback for Serious)
Subject: [URGENT] Emergency incident reported — Level 3 — Warehouse B
Location: Level 3 — Warehouse B
Severity: Emergency
Time: 2026-04-22 14:33
Reference: WH3-4F2
What was reported:
[Full notes text — not truncated]
AI Assessment:
Category: slip-trip-fall
Injury risk: Likely
Urgency: Yes
View full report →
If this is a life-threatening emergency, call 000.

The "call 000" line is non-optional on Emergency severity. Duty of care consideration.

🗄️ Schema
-- Locations registered in the system
CREATE TABLE locations (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name        TEXT NOT NULL,
  address     TEXT,
  slug        TEXT UNIQUE,           -- optional friendly short code
  qr_url      TEXT,                  -- generated QR image URL
  created_at  TIMESTAMPTZ DEFAULT now()
);

-- Submitted incidents
CREATE TABLE incidents (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  location_id   UUID REFERENCES locations(id) NOT NULL,
  severity      TEXT CHECK (severity IN ('low','medium','high')) NOT NULL,
  notes         TEXT NOT NULL,
  ai_result     JSONB,               -- populated async after submission
  submitted_at  TIMESTAMPTZ DEFAULT now(),
  notified      BOOLEAN DEFAULT false,
  reference     TEXT UNIQUE          -- 6-char alphanumeric e.g. "WH3-4F2"
);

-- Admin users (Supabase Auth handles passwords + sessions)
CREATE TABLE admin_profiles (
  id            UUID PRIMARY KEY REFERENCES auth.users(id),
  full_name     TEXT,
  email         TEXT,
  role          TEXT DEFAULT 'admin', -- future: 'superadmin', 'viewer'
  notify_sms    BOOLEAN DEFAULT false,
  phone         TEXT,
  created_at    TIMESTAMPTZ DEFAULT now()
);

-- Notification audit trail
CREATE TABLE notification_log (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  incident_id   UUID REFERENCES incidents(id),
  channel       TEXT,                -- 'email' | 'sms' | 'push'
  sent_at       TIMESTAMPTZ DEFAULT now(),
  success       BOOLEAN,
  error_message TEXT
);

-- Retry queue for failed AI analysis
CREATE TABLE failed_ai_jobs (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  incident_id   UUID REFERENCES incidents(id),
  attempts      INT DEFAULT 0,
  last_attempt  TIMESTAMPTZ,
  error         TEXT
);
🔐 Row-Level Security
TablePublic (unauthenticated)Admin (authenticated)
locationsSELECT (by ID only)Full access
incidentsINSERT onlySELECT + UPDATE
admin_profilesNo accessOwn row only
notification_logNo accessSELECT

RLS enforced by Supabase — no custom auth middleware needed on the reporter path. Reporter submits without logging in.

🚫
Build cannot start until Q1, Q2, and Q5 are answered

These three questions have direct architectural impact. Schema design, data model, and notification logic all depend on the answers. The remaining questions are important but can be decided in parallel with early build work.

Q1 — MOST ARCHITECTURALLY IMPACTFUL
Multi-tenancy: single organisation or a SaaS product?
Is this system for one organisation (all data shared by one team) — or will it serve multiple organisations, each with their own isolated locations, admins, and data?

Multi-tenancy is fundamentally different at the database level. Every table needs a tenant_id column. RLS policies change. The URL scheme changes. Admin invite flows change. This cannot be retrofitted after build begins.
High Blocks database schema design
Q2
Anonymous vs identified reporters
Does the system capture who submitted a report?

Option A — Fully anonymous: No name field. Reference number is the only link. Maximises reporting volume.
Option B — Optional identity: Name/phone field shown but not required. Recommended balance.
Option C — Named (required): Name field required. Adds friction — expect lower incident reporting rates.

Recommendation: Option A or B. Under-reporting of minor incidents and near-misses is the most common failure mode in safety reporting. Any friction that reduces reporting frequency costs more than the identity data is worth.
Medium Affects schema, reporter UX, and privacy obligations
Q3
Severity level names and count
Current proposal: Minor / Serious / Emergency.

Some organisations use: Low / Medium / High — or — Near Miss / Incident / Emergency — or — Green / Amber / Red.

Do you need a 4th level (e.g. "Near Miss" as separate from "Minor Incident")?

Note: adding a fourth level is not just a label change — it changes notification thresholds, the data model, and all dashboard charts.
Medium Affects notification logic and dashboard design
Q4
Notification threshold for "Serious" severity
Should ALL "Serious" submissions notify admin? Or only those where the AI also detects injury risk?

Current proposal: Serious + AI injury risk triggers a notification. Serious alone is logged without a push — reduces alert fatigue.

If your organisation's policy is that all serious incidents require immediate human acknowledgement, the threshold should be raised.
Low Config-level decision — can be changed after launch
Q5 — LEGAL / COMPLIANCE
Data retention policy
Workplace incident records in Australia typically require 5–7 years retention under WHS Act obligations (which vary by state).

Confirm: What retention period applies? Does it vary by severity? Do you need a deletion workflow (relevant if personal data is captured via Q2 Option B/C)?

This must be designed into the database from the start. Retrofitting a retention policy is expensive.
High Blocks launch — legal/compliance must confirm
Q6
AI accuracy expectations and compliance requirements
The AI analysis is advisory only. Confirm:
— Is occasional misclassification acceptable (e.g. a vehicle incident categorised as equipment failure)?
— Are there regulatory or industry compliance constraints on using AI to analyse safety records?
— Does the AI analysis need to be auditable (could a regulator ask "how did the system classify this event")?

If classification must be auditable, a model logging layer is required from day one.
Medium Affects AI logging architecture
Q7
Go-live scope and logistics
Not blocking the build — but blocking go-live planning:
— How many locations will be registered at launch?
— How many admin users are needed?
— Is there a phased rollout (one building first, then site-wide)?
— Who is responsible for printing and posting QR codes at each location?
Low Operational — doesn't block build
🗺️ Delivery Plan — Three Phases
ℹ️
Total estimated timeline: 11–15 weeks from Q1/Q2/Q5 sign-off

Phase 1 cannot be skipped or compressed — the decisions made here directly control the database schema. Starting code before Phase 1 is complete guarantees rework. Each phase ends with a gate that must be passed before the next phase begins.

1

Specification & Design

Lock all decisions, define every screen, produce a spec document the build team can execute without guessing
2 weeks
Stage 1.1
Stakeholder Decision Session
Answer all blocking questions before a single line of code is written
Decisions required (from Open Questions)
Q1 — Tenancy model: Single organisation or multi-tenant SaaS? Determines the entire database schema. Must be answered first.
Q2 — Reporter anonymity: Fully anonymous / optional identity / required identity? Affects schema, UX, and privacy obligations.
Q5 — Data retention: Confirm retention period (typically 5–7 years under WHS Act). Must be in schema from day one.
Q3 — Severity levels: Minor / Serious / Emergency — or a different set? 3 or 4 levels?
Q4 — Notification threshold: All Serious incidents notify, or only those with AI injury risk?
Q6 — AI auditability: Does the AI classification need to be auditable for regulators?
Q7 — Launch logistics: Number of locations, number of admins, phased rollout plan?
Deliverable
Decision log document — every question answered, owner named, date recorded Client and dev lead sign off in writing before Stage 1.2 begins
Stage 1.2
UX Design — All Screens
High-fidelity wireframes for every user type before any frontend code begins
Reporter screens
Report form — location header, notes field, voice button, severity picker, submit Must pass the "stressed worker on a phone" test
Voice input screen — mic active state, live transcript, "Done speaking" button
Confirmation screen — success state, reference number, severity-appropriate message
Error states — invalid QR code, network failure (form preserved), submission failed
Admin screens
Incident list — filter by severity, status, location, date range; sortable; realtime badge on new arrivals
Incident detail — full notes, AI analysis panel (collapsed/expanded), status controls, internal notes
Location management — list, create/edit, QR download button, incident count per location
Admin notification preview — what the push and email actually look like on device
Management screens
Dashboard — KPI tiles, volume chart, by-location chart, category donut, severity trend line
Date range selector + drill-down — tap a location bar to filter all modules
Design system
Colour tokens — brand colours, severity colours (green/amber/red), background, surface
Component library — buttons, inputs, severity picker, notification cards, status badges, AI panel
Mobile-first constraints — minimum touch targets (48px), font sizes, thumb-zone layout rules
Stage 1.3
Technical Specification
Finalise schema, API contracts, Edge Function behaviour, and notification copy — the build team's source of truth
Database
Final schema — incorporating Q1 (tenancy), Q2 (anonymity), Q5 (retention), Q3 (severity levels)
RLS policy matrix — every table, every role, every operation explicitly defined
Reference number format — confirm format (e.g. WH3-4F2), uniqueness strategy, length
API contracts
POST /api/incidents — request body, validation rules, response shape, error codes
GET /report/[locationId] — what happens when location is invalid, inactive, or archived
Admin API routes — list incidents, get incident, update status, CRUD locations
AI pipeline spec
GPT-4o prompt — finalise the system prompt, output schema, category enum values Test on 20 synthetic incident reports before build begins
Escalation rules — exact conditions that trigger email, SMS, push (from Q4 decision)
Failure handling — retry strategy, max attempts, failed_ai_jobs table behaviour
Notification content
Email templates — subject lines, body copy, call-to-action links for each severity level
SMS copy — 160-char constraint, must include reference number and location
Push copy — title, body, deep link scheme for each notification type
🛑
Phase 1 Gate — Client & Architect Sign-off Required

Phase 2 does not begin until all three of the following are complete:

  • Decision log signed off by client
  • All screens approved by client (UX review)
  • Technical spec approved by Pete (architecture review)
Phase 1 complete → Phase 2 begins
2

Core Build

The full reporter flow, AI pipeline, notification system, and admin dashboard — the product's entire value chain, end to end
6–8 weeks
Stage 2.1
Foundation & Infrastructure
Project scaffolding, auth, CI/CD, base schema — before any feature work begins
Project setup
Next.js 16 (App Router) + TypeScript — initialised, linted, type-checked, committed to repo
Vercel project connected — auto-deploy on main, preview URLs on PRs, environment variables provisioned
Supabase project created — prod + staging environments, service role keys stored in Vercel secrets
Tailwind CSS 4 configured — design system tokens from Phase 1 spec implemented as CSS custom properties
Database
Initial migration applied — all tables from spec (locations, incidents, admin_profiles, notification_log, failed_ai_jobs)
RLS policies applied — tested against the policy matrix from Stage 1.3 before any feature is built on top
Seed data — sample locations and incidents for development use
Auth
Supabase Auth for admin — email/password login, session management, server-side JWT validation
Admin middleware — protect all /admin routes, redirect unauthenticated requests to login
Reporter route confirmed public — /report/[locationId] explicitly excluded from auth middleware
Stage 2.2
Reporter Flow
The public-facing QR → form → submit → confirm path. The most user-critical piece of the product.
Core flow
/report/[locationId] page — server component validates locationId against Supabase, renders location name prominently
Invalid location page — graceful error, not a broken form; suggests contacting a safety officer
SeverityPicker component — three pill buttons, no default, minimum 48px height, keyboard accessible
POST /api/incidents route handler — validates input, inserts to Supabase, returns 200 + reference number in <2 seconds
Confirmation screen — checkmark, reference number, severity-appropriate message copy
Network error handling — form content preserved on failure; retry button; no data loss
Voice input
SpeechInput component — wraps Web Speech API, real-time transcript display, "Done speaking" finalises
Browser support detection — falls back to text-only with message "Voice not available — please type your report."
Transcript merge — voice text flows into the same notes field as typed text; reporter can edit before submitting
Testing milestone
E2E test: full reporter flow — Playwright test covers QR URL → form fill → submit → confirmation on iOS Safari + Android Chrome Must pass before Stage 2.3 begins
Stage 2.3
AI Analysis Pipeline
Async Edge Function triggered on every submission — classification, risk flagging, DB write-back
Edge function
analyse-incident Edge Function — triggered by Supabase DB webhook on INSERT to incidents table
OpenAI GPT-4o call — structured output mode with the finalised prompt and output schema from Stage 1.3
ai_result JSONB write-back — all 8 fields (severity_assessment, injury_risk, incident_category, persons_affected, urgency_signal, summary_sentence, confidence_score, recommended_response)
Reliability
Failure handling — on OpenAI error: incident remains stored, ai_result stays null, entry added to failed_ai_jobs
Retry job — cron or Supabase scheduled function retries failed_ai_jobs up to 3 times at 5-minute intervals
Timeout guard — Edge Function enforces a 20-second timeout; timeout logged as failure, not silent hang
Testing milestone
20-case AI validation — run the finalised prompt against 20 synthetic incidents (from 5 categories, 3 severity levels, edge cases). Pass rate ≥ 90% before connecting to production notification triggers. AI failures in staging must be caught here, not after go-live.
Stage 2.4
Notification System
Email, SMS, and realtime push — the chain that gets an incident in front of a human within 10 seconds
Email (Resend)
Resend API integration — from domain verified, sender address configured, API key in Vercel env
Email templates — HTML templates for each severity level (Minor: logged-only, Serious: alert, Emergency: urgent + call 000)
Deep link in email body — direct link to incident detail in admin dashboard
SMS (Twilio)
Twilio integration — account, phone number, API key, 160-char message format (Emergency only)
Admin opt-in — notify_sms flag in admin_profiles; SMS only sent if admin has opted in and has a phone number
Realtime push
Supabase Realtime subscription — admin dashboard subscribes to incidents table changes; new urgent incident shows toast notification
Browser push (Phase 2) — Web Push API for background notifications when admin has dashboard closed. Not in v1 — email + realtime is sufficient.
Audit trail
notification_log writes — every send attempt (success or failure) logged with timestamp, channel, and error if applicable
Stage 2.5
Admin Dashboard
Incident list, detail view, AI panel, status management — the admin's primary workspace
Incident list
Incident list page — sortable by date, filterable by severity / status / location; pagination or infinite scroll
Severity badges — colour-coded (green/amber/red) with AI contradiction flag if AI severity differs from reporter's
Realtime new incident badge — "X new incidents" banner appears when Supabase Realtime fires; tap to refresh
Incident detail
Detail page — location, time, severity, full notes above the fold
AI analysis panel — collapsed by default, expandable; shows all 8 AI fields; confidence caveat if score < 0.5
Status controls — Open / In Progress / Closed pill buttons; one tap + confirm dialog; timestamp logged on change
Internal notes — admin-only freetext, not visible to reporter; timestamped; multiple entries
Testing milestone
End-to-end integration test — submit incident via reporter form → confirm AI analysis fires → confirm email/SMS delivered → confirm status update logged. Full chain must pass before Phase 3 begins.
🛑
Phase 2 Gate — Internal QA Review

The following must pass before Phase 3 begins:

  • Reporter E2E test green (iOS Safari + Android Chrome)
  • AI 20-case validation ≥ 90% pass rate
  • Full notification chain integration test passes (email + push)
  • Admin dashboard functional review with Pete
  • RLS policies tested — attempt to access data across tenants (if multi-tenant) must fail
Phase 2 complete → Phase 3 begins
3

Management Layer, QR & Launch

Location management, QR generation, management reporting, hardening, and go-live
3–4 weeks
Stage 3.1
Location Management & QR Generation
Admins create locations and print QR codes — the physical deployment mechanism
Location CRUD
Location list — name, address, incident count (this month), last incident date, QR status (active/inactive)
Create / edit location — name, address, optional slug, active/inactive toggle
Archive location — soft delete; existing incidents preserved; QR code returns graceful "this location is no longer active" message
QR code generation
QR generation API — uses qrcode npm library; encodes full https:// URL; returns PNG
Print-ready download — 300 DPI PNG with location name printed below the code; suitable for A4 or A5 laminated sheet
QR preview in admin — admin can verify the QR decodes correctly before printing
Stage 3.2
Management Reporting
The five dashboard modules that give leadership their safety trend picture
Dashboard modules (Recharts or Tremor)
Module 1 — Summary KPIs — total incidents, vs prior period (delta + colour), emergency count, locations with zero incidents
Module 2 — Volume over time — stacked bar chart, weekly bins, severity colour-coded, date range selector
Module 3 — By location — horizontal bar chart ranked by volume, colour = most serious incident, tap to filter
Module 4 — Category breakdown — donut chart of AI-assigned categories, tap segment to see count and %
Module 5 — Severity trend — three-line chart (Minor / Serious / Emergency) over time
Access controls
Management role — separate permission level; cannot see individual incident notes or reporter identity
RLS confirmed — management queries aggregate only; no row-level access to incidents table text fields
Stage 3.3
Testing & Hardening
Security, accessibility, cross-browser, and performance — before any real location data enters the system
Security
RLS penetration test — authenticated admin attempts to access another tenant's data (if multi-tenant); must fail cleanly
Injection testing — submit incident notes with SQL injection, XSS payloads; confirm parameterised queries and output encoding hold
Rate limiting — POST /api/incidents limited to prevent bulk spam submissions from a single IP
QR code substitution scenario — confirm the system handles an attacker's QR pointing to an invalid locationId gracefully (not an exploitable path)
Accessibility
Reporter form audit — keyboard navigation, screen reader labels, focus management, contrast ratios on all severity states
WCAG 2.1 AA compliance — reporter and admin screens; management dashboard at best-effort
Cross-browser & performance
iOS Safari 15+ (voice) — Web Speech API and form behaviour confirmed on physical iPhone, not just simulator
Android Chrome (voice) — tested on physical Android device in a noisy environment
Submission under load — 50 concurrent submissions; confirm no race conditions in reference number generation; confirm AI queue does not back up
Slow network test — reporter flow tested at 3G speeds; confirm <2 second submission target is met
Stage 3.4
Launch Preparation
Everything that needs to happen before the first real QR code is printed and posted
Environment
Production domain — custom domain configured on Vercel, HTTPS auto-provisioned, DNS propagated
Resend sender domain verified — SPF, DKIM, DMARC set up on the sending domain to avoid spam flags
Twilio number confirmed — Australian number provisioned, tested end-to-end on production stack
Error tracking (Sentry) — production DSN configured, test error confirmed in Sentry dashboard before go-live
Content & operational readiness
Admin accounts created — all admin users onboarded, passwords set, notify_sms preferences configured
Locations created — all launch locations entered, QR codes generated and downloaded
QR codes printed and laminated — client responsibility; confirm format (A5, landscape) and posting position at each location
Admin walkthrough session — live demonstration with all admin users; how to receive alerts, review an incident, update status
Admin quick-reference card — one-page PDF: how to read an incident, what each severity means, how to change status, who to call for technical issues
Go-live checklist
Smoke test on production — developer submits a test incident from a QR code, confirms AI fires, confirms email arrives, confirms it appears in admin dashboard
Test incident deleted — remove test data from production DB before admins start seeing the incident list
Stage 3.5
Go-Live & Hypercare
Deployment, monitoring, and a two-week support window while the team builds confidence
Deployment
Production deploy — merge to main → Vercel auto-deploys → confirm production URL is live
QR codes active — client posts QR codes at all launch locations on day one
Monitoring (weeks 1–2)
Sentry error monitoring — daily check of Sentry dashboard; any new error type investigated same day
AI failure monitoring — daily check of failed_ai_jobs table; any failures from real incidents investigated and manually retried
Notification audit — spot-check notification_log to confirm emails and SMS are delivering successfully
Admin feedback loop — brief check-in call at day 3 and day 10 with admin users; surface any confusion or missing features
Hypercare end
Handover to support — after two weeks, system moves from hypercare to standard support SLA; any outstanding items logged as Phase 2 tickets
Launch Gate — Ready for First Real Incident

The system is in production when ALL of the following are confirmed:

  • Smoke test on production passed (real QR → real submission → real email → dashboard visible)
  • All admin users onboarded and notified
  • QR codes physically posted at all launch locations
  • Error monitoring active (Sentry + failed_ai_jobs check)
  • Hypercare rota agreed (who is on call for first two weeks)
📅 Timeline Summary
Phase Duration Starts when Gate to pass
Phase 1 — Specification & Design 2 weeks Q1, Q2, Q5 answered by client Client sign-off on UX + spec; Pete approves architecture
Phase 2 — Core Build 6–8 weeks Phase 1 gate passed E2E tests green; AI validation ≥ 90%; notification chain confirmed
Phase 3 — Management + Launch 3–4 weeks Phase 2 gate passed Smoke test passes; QR codes posted; admin users onboarded
Total 11–14 weeks From Q1/Q2/Q5 decision
Add comments or questions to any section, then export below.

Review Summary — Site Incident Reporter

✓ Copied to clipboard.