feat: implement incident reporting and management system
- Add incident reporting routes and logic to handle creation, updating, and retrieval of incidents. - Implement visibility rules for users based on roles (reporters, supervisors). - Create tests for incident reporting functionality to ensure correctness and reliability. - Introduce site and area management routes, including validation for area hierarchy. - Establish user management routes with role-based access control for creating and updating users. - Add setup routes for initializing the application with an organization and admin user. - Implement shared enums and schemas for consistent validation and data handling across the application. - Create a test helper for setting up an in-memory database and API client for testing. - Add TypeScript configuration for server-side code with strict type checking and module resolution.
This commit is contained in:
1 parent
a382fa904a
commit
c69eccb5c1
47 files changed
+6297
-45
No files matched your search
+77
@@ -0,0 +1,77 @@
|
||||
# API reference
|
||||
|
||||
JSON over HTTP under `/api`. Authentication uses a session cookie (`safetylog_session`, http-only) set by setup, sign-in and password change.
|
||||
|
||||
Request bodies are validated against the schemas in [`shared/schemas.ts`](../shared/schemas.ts). Errors look like `{ "error": "message" }`; validation errors (400) also include `issues: [{ path, message }]`.
|
||||
|
||||
Roles, from least to most privileged: `reporter` < `supervisor` < `safety_manager` < `admin`. A role can do everything the roles below it can.
|
||||
|
||||
## Setup and authentication
|
||||
|
||||
| Method | Path | Who | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| GET | `/api/health` | anyone | `{ ok: true }` |
|
||||
| GET | `/api/setup` | anyone | `{ needsSetup }`: true until the first admin exists |
|
||||
| POST | `/api/setup` | anyone, once | `{ organizationName, name, email, password }`. Creates the organization and an admin, signs them in. 409 afterwards |
|
||||
| POST | `/api/auth/login` | anyone | `{ email, password }`. 401 on bad credentials, 429 after 10 attempts per email in 15 minutes |
|
||||
| POST | `/api/auth/logout` | anyone | Ends the session. 204 |
|
||||
| GET | `/api/auth/me` | signed in | `{ user, organization }` |
|
||||
| POST | `/api/auth/password` | signed in | `{ currentPassword, newPassword }`. Signs out all other sessions. 204 |
|
||||
|
||||
Passwords need at least 10 characters.
|
||||
|
||||
## Users
|
||||
|
||||
| Method | Path | Who | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| GET | `/api/users` | safety_manager | All users in the organization |
|
||||
| POST | `/api/users` | admin | `{ name, email, role, password }`. 409 if the email exists |
|
||||
| PATCH | `/api/users/:id` | admin | `{ name?, role?, active? }`. Deactivating signs the user out. The last active admin can't be demoted or deactivated |
|
||||
|
||||
## Sites and areas
|
||||
|
||||
| Method | Path | Who | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| GET | `/api/sites` | signed in | Sites with their `areas` (flat list; build the tree from `parentId`). `?includeArchived=true` to include archived |
|
||||
| POST | `/api/sites` | safety_manager | `{ name, address?, timezone? }` |
|
||||
| PATCH | `/api/sites/:id` | safety_manager | `{ name?, address?, timezone?, archived? }` |
|
||||
| POST | `/api/sites/:id/areas` | safety_manager | `{ name, kind?, parentId? }`. `kind`: `building`, `area` (default), `line`, `other`. Parent must be in the same site |
|
||||
| PATCH | `/api/areas/:id` | safety_manager | `{ name?, kind?, archived? }` |
|
||||
|
||||
## Incidents
|
||||
|
||||
Incidents are addressed by their number (`/api/incidents/42` is `INC-0042`). Numbers are sequential per organization with no gaps.
|
||||
|
||||
| Method | Path | Who | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| GET | `/api/incidents` | signed in | Paginated `{ items, page, pageSize, total }`, newest first. Reporters see only their own reports |
|
||||
| POST | `/api/incidents` | signed in | Create a report (below). 201 |
|
||||
| GET | `/api/incidents/:number` | signed in | 404 if it doesn't exist or isn't visible to you |
|
||||
| PATCH | `/api/incidents/:number` | supervisor | Any create field plus `status`. Closing sets `closedAt`; reopening clears it. Moving to another site clears the area unless one is given |
|
||||
|
||||
**List query parameters:** `page` (default 1), `pageSize` (1–100, default 25), `q` (searches title, description and location; `INC-0042` or `42` matches the number), `type`, `severity`, `status`, `siteId`.
|
||||
|
||||
**Create body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Forklift reversed into pallet stack",
|
||||
"type": "near_miss",
|
||||
"severity": "high",
|
||||
"occurredAt": "2026-10-06T08:15:00-05:00",
|
||||
"siteId": "…",
|
||||
"areaId": "…",
|
||||
"locationDetail": "Loading bay C",
|
||||
"description": "…",
|
||||
"immediateAction": "Area cordoned off",
|
||||
"anonymous": false
|
||||
}
|
||||
```
|
||||
|
||||
- `type`: `near_miss`, `hazard`, `injury`, `illness`, `property_damage`, `environmental`
|
||||
- `severity`: `low`, `medium`, `high`, `critical`
|
||||
- `status`: `reported`, `triage`, `investigation`, `actions`, `verification`, `closed`
|
||||
- `occurredAt` must include a time-zone offset and can't be in the future.
|
||||
- Anonymous reports don't record the reporter at all, so they won't appear in the reporter's own list.
|
||||
|
||||
Every create and update is recorded in the audit log (`audit_log` table) with who made it and what changed.
|
||||
+18
-8
@@ -10,8 +10,10 @@ This is a living document. Update it as decisions are made or scope changes.
|
||||
| --- | --- | --- |
|
||||
| License | AGPL-3.0-only | Decided |
|
||||
| Frontend | React + Vite + TypeScript, Tailwind CSS v4 | Decided |
|
||||
| Backend | Node (Fastify or Hono) + PostgreSQL + Drizzle ORM, shared Zod schemas | Proposed |
|
||||
| Deployment | Self-hosted, single `docker compose up` (app + Postgres + file storage) | Proposed |
|
||||
| Backend | Node + Hono, PostgreSQL via Drizzle ORM, Zod schemas shared with the web app (`shared/`). Node runs the TypeScript directly (no build step for the server) | Decided |
|
||||
| Database | PostgreSQL in production; embedded PGlite for local development, tests and very small installs | Decided |
|
||||
| Auth | Email + password (scrypt), server-side sessions in an http-only cookie, four roles: reporter < supervisor < safety manager < admin | Decided |
|
||||
| Deployment | Self-hosted, single `docker compose up` (app + Postgres); file storage added with photo upload | Decided |
|
||||
| Compliance scope | US OSHA first; data model kept extensible for other regions (e.g. RIDDOR) | Proposed |
|
||||
| CI | Gitea Actions (`.gitea/workflows`): lint, typecheck, unit tests, build, e2e | Decided |
|
||||
| Product name | Open SafetyLog EHS (replaced placeholder "IA Tracker" and the prototype's "Safesite" branding) | Decided |
|
||||
@@ -32,13 +34,21 @@ This is a living document. Update it as decisions are made or scope changes.
|
||||
|
||||
## Phase 1: Backend and data model
|
||||
|
||||
- Entities: Organization, Site → Area (location tree), User, Role, Incident (type, severity, status, timestamps, reporter or anonymous), InjuredPerson and injury details, Attachment, Investigation, RootCause, CorrectiveAction, Comment, AuditLog, HoursWorked
|
||||
- Auth: email/password and magic links; SSO/OIDC later; role-based permissions
|
||||
- REST API with pagination, filtering and sequential incident numbering
|
||||
- [x] Database schema and first migration for all entities: Organization, Site → Area (location tree), User, Session, Incident, InjuredPerson, Attachment, Investigation, RootCause, CorrectiveAction, Comment, AuditLog, HoursWorked
|
||||
- [x] First-run setup (creates the organization and first admin)
|
||||
- [x] Email/password sign-in, sessions, change password, login rate limiting
|
||||
- [x] Role-based permissions (reporters see only their own reports)
|
||||
- [x] REST API: users, sites/areas, incidents with pagination, search, filters and gap-free sequential numbers (`INC-0001`); see [API.md](API.md)
|
||||
- [x] Audit log of every change
|
||||
- [x] Server tests (Vitest + in-memory PostgreSQL), demo seed data
|
||||
- [x] Dockerfile and `docker-compose.yml`
|
||||
- Moved to Phase 2: magic-link sign-in and password reset by email (both need the email sending built for notifications). SSO/OIDC stays "later".
|
||||
|
||||
## Phase 2: Core workflow (MVP)
|
||||
|
||||
- Reporting that saves to the database, with photo upload
|
||||
- Connect the web app to the API: sign-in and first-run setup screens, incident list and report form using real data
|
||||
- Photo upload (attachments)
|
||||
- Magic-link sign-in and password reset by email
|
||||
- Incident detail page: timeline, investigation, root-cause tools (5 Whys, fishbone), corrective actions (CAPA)
|
||||
- Status workflow: Reported → Triage → Investigation → Corrective actions → Verification → Closed, with role-based transitions
|
||||
- "My actions" list and overdue tracking
|
||||
@@ -75,8 +85,8 @@ This is a living document. Update it as decisions are made or scope changes.
|
||||
|
||||
Inspections and audit checklists, job safety analysis (JSA), training and certification records, management of change, SDS links, public API and webhooks.
|
||||
|
||||
## Known prototype gaps (to be resolved in Phases 1–2)
|
||||
## Known prototype gaps (to be resolved in Phase 2)
|
||||
|
||||
- All data is in memory and resets on refresh
|
||||
- The web app still uses in-memory demo data; the API exists but isn't connected yet
|
||||
- Dashboard, analytics and location figures are hardcoded
|
||||
- Export, Add location, Settings, Resources, search, notifications and workspace switcher are non-functional
|
||||
+9
-1
@@ -4,12 +4,20 @@ Open items that aren't tied to a single roadmap phase. Tick them off (or delete
|
||||
|
||||
## Project setup
|
||||
|
||||
- [ ] **Code of Conduct contact.** Replace the placeholder in [CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md) with a reporting email address, ideally a project alias rather than a personal one.
|
||||
- [ ] **Code of Conduct contact.** [CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md) uses `jay@equineform.com` for now. Change it to a project alias (e.g. `conduct@<project-domain>`) once the project has its own domain.
|
||||
- [ ] **Source code link.** The About page links to `https://www.equineform.com` for now (default in `src/lib/config.ts`, overridable with `VITE_SOURCE_URL`). Change it to the public git repository once one exists, and use the same URL for issue links in CONTRIBUTING.md.
|
||||
- [ ] **CI on Gitea.** Confirm Actions is enabled for the repository and a runner is registered, then check the first run of `.gitea/workflows/ci.yml` passes. Keep the Playwright container tag in the workflow in sync with `@playwright/test`.
|
||||
|
||||
## Phase 1 follow-ups
|
||||
|
||||
- [ ] **Test against real PostgreSQL.** Server tests use PGlite (PostgreSQL compiled to WebAssembly). Add a CI job with a `postgres` service container that runs the server tests against real PostgreSQL too.
|
||||
- [ ] **Docker build in CI.** Build the image in CI once a runner with Docker is available.
|
||||
- [ ] **Shared login rate limit.** The login limiter is in memory, so it resets on restart and isn't shared between processes. Fine for one container; move it to the database before running several app instances.
|
||||
|
||||
## Done
|
||||
|
||||
- [x] Docker verified 2026-10-06 (Docker 29.8, Compose v5.5): image builds (~315 MB), `docker compose up` with PostgreSQL 18 passes setup → sign-in → report smoke test; data and sessions survive `down`/`up`; clean shutdown; healthcheck healthy; runs as non-root; standalone PGlite container works; `Secure` cookie with an `https://` PUBLIC_URL; web app loads with no errors.
|
||||
|
||||
- [x] Product name: **Open SafetyLog EHS** (decided 2026-10-06). Note: Insight Works sells a similarly named free "Safety Logbook" EHS app for Microsoft Business Central; run a USPTO trademark search before a public launch.
|
||||
- [x] CONTRIBUTING.md and CODE_OF_CONDUCT.md (Contributor Covenant 2.1)
|
||||
- [x] About page with source-code link (AGPL-3.0 section 13)
|
||||
Reference in new issue
Block a user