---
type: project
status: active
tags: [project, cpas, hr, internal-tool, docker, react, node, sqlite]
repo: https://git.alwisp.com/jason/cpas
started: 2026-03-06
updated: 2026-05-14
owner: Jason Stedwell
---

# CPAS Violation Tracker

Single-container Dockerized web app for **CPAS** (workforce standing / violation documentation) used internally at Message Point Media. Replaces ad-hoc paper / spreadsheet violation tracking with a structured, auditable system that produces signed PDF records and surfaces tier-escalation risk live.

> Repo: [git.alwisp.com/jason/cpas](https://git.alwisp.com/jason/cpas) · First commit: 2026-03-06 · Deployed on Unraid (`10.2.0.14:3001`)

## Stack

- **Frontend:** React + Vite (single-page app, dark theme, mobile-responsive at 375px+)
- **Backend:** Node.js + Express
- **DB:** SQLite via `better-sqlite3`, WAL mode, auto-migrations on boot
- **PDF:** Puppeteer + bundled Chromium (system install inside container)
- **Packaging:** One multi-stage Dockerfile — build React, install backend, bundle Chromium, ship as a single image
- **Runtime requirement on dev box:** Docker Desktop only. No host Node/npm needed.

## What it does

It manages **employee violations against the CPAS rubric** on a rolling 90-day window:

- **Company Dashboard** — every employee sorted by active CPAS points (highest risk first), with summary stat cards, tier badges, an "at-risk" flag (within 2 pts of next tier), and search + department filter.
- **Violation Form** — pick an employee, pick a violation type, see prior 90-day count inline; recidivist auto-escalation; pre-submit tier-crossing warning; context-aware fields; one-click PDF download on submit; optional employee acknowledgment block.
- **Employee Profile Modal** — full violation history, amendment count, edit employee, merge duplicate, negate/restore, hard delete, per-violation PDF, free-text notes/flags ("on PIP", "union member"), a per-violation 90-day **point expiration timeline** with projected tier drops, and a **↻ Backfill Snapshots** repair button that rebuilds the `prior_active_points` snapshot on every violation for that employee (use after a back-date if older PDFs show stale prior-point totals; audit-logged with reason `manual_backfill`).
- **Violation Amendment** — point value / type / incident date are immutable; non-scoring fields (location, witness, narrative, acknowledgment) are amendable with a field-level diff trail.
- **Audit Log** — append-only record of every write action (employee CRUD, violation logged/amended/negated/restored/deleted); filterable, paginated panel from the dashboard.
- **Toast notification system** — global success/error/warning/info, auto-dismiss with progress bar.
- **PDF generation** — Puppeteer, snapshot of prior active points at incident time, optional employee acknowledgment block on the signature page.
- **Stakeholder demo** — `/demo` static route with synthetic data, served before the SPA catch-all; no auth required.

## CPAS Tier System

| Points | Tier | Label                   |
| ------ | ---- | ----------------------- |
| 0–4    | 0–1  | Elite Standing          |
| 5–9    | 1    | Realignment             |
| 10–14  | 2    | Administrative Lockdown |
| 15–19  | 3    | Verification            |
| 20–24  | 4    | Risk Mitigation         |
| 25–29  | 5    | Final Decision          |
| 30+    | 6    | Separation              |

Scores are summed over a **rolling 90-day window**; negated violations excluded.

## Database

Six tables + one view:

- `employees` — id, name, department, supervisor, notes
- `violations` — full incident record, including `prior_active_points` snapshot
- `violation_resolutions` — soft-delete reason / details
- `violation_amendments` — field-level diff log (one row per changed field)
- `audit_log` — append-only system action log
- `active_cpas_scores` (view) — 90-day point sum per employee

Auto-migrations in `db/database.js` add new columns to existing DBs on startup — meaningful here because Jason runs this in production on Unraid, so the schema evolves without losing data.

## Deployment notes worth remembering

- **Unraid:** static IP on `br0` bridge (`10.2.0.14`), DB persisted at `/mnt/user/appdata/cpas/db/cpas.db`, WebUI on port 3001.
- **`--pids-limit 2048` is critical** — Puppeteer/Chromium spawns many processes for each PDF; Unraid's default cap silently kills PDF generation.
- **Volume:** `/mnt/user/appdata/cpas/db` → `/data`. Database survives rebuilds, image reloads, and container removal.
- Updates are a 3-step loop: `docker build` locally → `docker save | gzip` → SMB drop into appdata → `docker load` + restart container in Unraid GUI.

## Mobile

Responsive design targets 375px+ (iPhone SE and up). At ≤768px the dashboard table swaps to a card-based layout (`DashboardMobile.jsx`); the nav stacks; tap targets are ≥44px; form inputs use 16px font to prevent iOS focus zoom. No external CSS library — single `mobile.css` utility sheet + a `useMediaQuery` hook. Implementation details and testing checklist live in `MOBILE_RESPONSIVE.md`.

## Project structure

```
cpas/
├── Dockerfile                  # Multi-stage: React build + Express + Chromium
├── server.js                   # API + static SPA + /demo route
├── db/
│   ├── schema.sql              # Tables + 90-day active score view
│   └── database.js             # SQLite + auto-migrations
├── pdf/
│   ├── generator.js            # Puppeteer
│   └── template.js             # HTML PDF template
├── demo/                       # /demo synthetic-data SPA
└── client/                     # React + Vite frontend
    └── src/
        ├── App.jsx             # Root + footer (copyright, dev ticker, Gitea link)
        ├── data/
        │   ├── violations.js   # All CPAS violation definitions
        │   └── departments.js
        ├── hooks/useEmployeeIntelligence.js
        └── components/         # Dashboard, ViolationForm, EmployeeModal,
                                # AmendViolationModal, AuditLog, ToastProvider, etc.
```

## API surface (selected)

- `GET /api/health` — health + build version
- `GET /api/dashboard` — all employees with active points + violation counts
- `GET /api/employees/:id/expiration` — roll-off timeline with days remaining
- `POST /api/violations` — log violation (accepts `acknowledged_by`, `acknowledged_date`)
- `PATCH /api/violations/:id/amend` — non-scoring field amendment + diff log
- `PATCH /api/violations/:id/negated` / `/restore` — soft delete + restore
- `GET /api/violations/:id/pdf` — PDF download
- `GET /api/audit` — paginated audit log

## Status

Phase 8 of the public roadmap is complete (stakeholder demo + app footer with live dev ticker). Core HR documentation workflow is shipped: dashboard, violation entry, employee profile, amendments, audit log, expiration timeline, acknowledgment field, toast system, mobile layout.

## Notable open ideas (from roadmap)

**Quick wins (low effort):** column sort on dashboard, department multi-select filter, `N` keyboard shortcut for new violation, configurable at-risk threshold via env var, version.json injected at build time.

**Reporting:** violation trend chart (daily/weekly/monthly), department heat map, per-employee sparklines in profile modal.

**Workflow:** draft/pending violations before finalize, violation templates.

**Notifications:** tier-escalation alerts (email or in-app) on crossing into Tier 2+.

**Infra (higher effort):** multi-user auth with roles (currently runs on trusted internal LAN with none), scheduled DB backup, dark/light theme toggle.

## Review take

The app is well-scoped and visibly production-shaped, not a hobby project. Strong signals: append-only audit log, field-level amendment diffs, immutable scoring fields, `prior_active_points` snapshot baked into each violation row so historical PDFs stay accurate, and auto-migrations so the schema can evolve in place on a live Unraid deployment. The Unraid-specific install guide explicitly calls out `--pids-limit 2048` for Chromium, which is the kind of footgun that only gets documented after it's been hit in production — that detail alone tells you this is being actually used.

The single biggest gap relative to its current capability is **auth**. Everything else on the roadmap is incremental polish; multi-user auth with roles is the one change that meaningfully expands who can use the system safely. Worth pairing it with the proposed scheduled DB backup, since the moment more than one supervisor is writing, accidental damage gets more likely.

A secondary observation: the audit log + amendment diff infrastructure is already strong enough to support a "who changed what, when" view per employee — that's basically free reporting if surfaced in the profile modal as a timeline.

## Links

- Repo — [git.alwisp.com/jason/cpas](https://git.alwisp.com/jason/cpas)
- Local install guide — `README.md` (in repo)
- Unraid install guide — `README_UNRAID_INSTALL.md`
- Mobile implementation notes — `MOBILE_RESPONSIVE.md`
