> **The app requires login.** The admin account is created from `ADMIN_USERNAME` / `ADMIN_PASSWORD` on every start. The image ships a placeholder password (`changeme`) that **must** be overridden. To rotate the admin password later, change `ADMIN_PASSWORD` and restart. Additional users are created in-app by an admin (**Users** button, top-right).
> **Set a strong `ADMIN_PASSWORD`.** The app requires login and the admin account is created/re-synced from these env vars on every start. The image ships a placeholder (`changeme`) that must be overridden. To rotate the admin password later, edit this variable and **Restart** the container. Additional users are created in-app by an admin (**Users** button).
Access the app at `http://10.2.0.14:3001` (or whatever static IP you assigned).
### Key settings explained
| Setting | Value | Notes |
|---------|-------|-------|
| `--net` | `br0` | Unraid custom bridge network — gives the container its own LAN IP |
| `--ip` | `10.2.0.14` | Static IP on your LAN — adjust to match your subnet |
| `--pids-limit` | `2048` | Required — Puppeteer/Chromium spawns many processes for PDF generation; default Unraid limit is too low and will cause PDF failures |
| `PORT` | `3001` | Express listen port inside the container |
| Volume | `/mnt/user/appdata/cpas/db` → `/data` | Persists the database across container restarts and rebuilds |
### Updating on Unraid
1. Build and export the new image on your dev machine (Step 1 above)
2. Load it on Unraid: `docker load < cpas-latest.tar.gz`
3. Stop and remove the old container: `docker stop cpas && docker rm cpas`
4. Re-run the `docker run` command from Step 4 — the volume mount preserves all data
> **Note:** The `--pids-limit 2048` flag is critical. Without it, Chromium hits Unraid's default PID limit and PDF generation silently fails or crashes the container.
If you prefer the Unraid GUI over the CLI, load the image (`docker load < cpas-latest.tar.gz`), then **Docker → Add Container** (Advanced View on) and fill in the equivalent settings:
It is a static page (its own hardcoded data, no live API calls), served before the SPA catch-all, so it stays reachable without logging in. The live app and all `/api/*` data routes still require authentication — only this synthetic page is public. Useful for showing the app to stakeholders without exposing live employee data.
- **Login gate** on the entire app — every page and every `/api/*` route (except `/api/health` and the login endpoint) requires a valid session
- **Bearer-token sessions** with a 7-day lifetime; token stored client-side and attached to every request, cleared automatically on expiry
- **Bootstrap admin** created/synced on startup from the `ADMIN_USERNAME` / `ADMIN_PASSWORD` environment variables — rotate the password by changing the env var and restarting
- **Admin-managed users** — admins add/delete accounts and reset passwords from the in-app **Users** panel; two roles (`admin`, `user`)
- Passwords stored as scrypt hashes (no external auth dependency); logins and account changes are audit-logged
- **Custom violation types**: add or edit user-defined types directly from the form (`+ Add Type` / `Edit Type` buttons); persisted to the database and merged into the dropdown alongside hardcoded types
- **Financial amount tracking**: dollar amount in question recorded for chargeback / receipt / custom financial violations; surfaces on the PDF for repayment records and is audit-logged on edit
- **Employee Acknowledgment section**: optional "received by employee" name and date fields; when filled, the PDF signature block shows the recorded acknowledgment instead of a blank signature line
- **Amend** button per active violation — edit non-scoring fields (location, notes, witness, acknowledgment, etc.) with a full field-level diff history
- Negate / restore individual violations (soft delete with resolution type + notes)
- Hard delete option for data entry errors
- PDF download for any historical violation record
- **Notes & Flags** — free-text notes (e.g. "on PIP", "union member") with quick-add tag buttons; visible in the profile modal without affecting scoring
- **Point Roll-Off Timeline** — lists each upcoming roll-off event (5 points retire per completed 90-day clean cycle, oldest first), with a progress bar, days-remaining countdown anchored to the last violation date, projected post-roll-off score, and tier-drop indicators
- **↻ Backfill Snapshots** button (next to the Active Violations header) — manually rebuilds the `prior_active_points` snapshot on every violation for this employee. Use after back-dating a violation under older code, or any time a regenerated PDF shows stale prior-point totals. Audit-logged as `violation_snapshots_recomputed` with reason `manual_backfill`. See [Backfilling Prior-Points Snapshots](#backfilling-prior-points-snapshots) below.
- **Live dev ticker**: real-time elapsed counter since first commit (`2026-03-06`), ticking every second in `Xd HHh MMm SSs` format with a pulsing green dot
- **Gitea repo link** with icon — links directly to `git.alwisp.com/jason/cpas`
Scores follow a **clean-cycle roll-off** (negated violations excluded): points retire only after the employee goes a full **90 consecutive days with no new violation**, and any new violation resets that clock for their entire balance. Each completed clean cycle drops **5 points, oldest first**. This is deliberately *not* a per-violation "expires 90 days after its own date" window — the math lives in [`lib/rolloff.js`](lib/rolloff.js) (`computeStanding`).
- **Acknowledgment rendering**: if the violation has an `acknowledged_by` value, the employee signature block on the PDF shows the recorded name and date with an "Acknowledged" badge; otherwise, blank signature lines are rendered for wet-ink signing
- **Back-dated inserts** auto-refresh the snapshot on downstream violations whose 90-day prior window now includes the new earlier event (handled inside the insert transaction by `recomputeSnapshotsAfter()`). If a back-date happened under older code that lacked this auto-refresh, use the **↻ Backfill Snapshots** button in the Employee Profile Modal — see [Backfilling Prior-Points Snapshots](#backfilling-prior-points-snapshots).
---
## Backfilling Prior-Points Snapshots
Each violation stores a `prior_active_points` snapshot at insert time so its PDF always reflects the score *as it was on the incident date* (and stays stable through later negate/restore actions). Normally you never touch this column.
There is one situation where the snapshot can drift from current truth: a violation was back-dated *before*`recomputeSnapshotsAfter()` shipped (commit `e2c352d`), so the auto-refresh never ran on the violations that now sit inside its 90-day window. Symptom: re-downloading the PDF for the newer violation shows "Prior Active Points: 0" even though an earlier active violation clearly exists in the timeline.
**To fix:**
1. Open the affected employee's profile modal.
2. Click **↻ Backfill Snapshots** next to the **Active Violations** header.
3. Confirm the prompt. A toast reports `Updated X of Y snapshot(s)` or `Snapshots already up to date`.
4. Re-download the PDFs — they now reflect the corrected prior totals.
**What it does, exactly:**
- Iterates every violation belonging to that employee (active *and* negated).
- Recomputes each row's `prior_active_points` using the current set of non-negated violations in the 90 days before its `incident_date`.
- Writes only the rows that actually changed and reports the diff.
- Runs inside a single transaction.
- Writes one `violation_snapshots_recomputed` entry to the audit log with `reason: "manual_backfill"` and the per-row before/after values.
**When *not* to use it:**
- After a negate, restore, amend, or hard delete in normal workflow. The auto-managed snapshot is correct in those cases by design (PDFs are intentionally stable through negate/restore).
- As a routine maintenance step. It's a targeted repair tool, not a recurring task. If you find yourself reaching for it after normal back-dated inserts, file a bug — the auto-recompute should already be handling those.
| POST | `/api/employees/:id/recompute-snapshots` | Manual backfill — rebuild `prior_active_points` on every violation for this employee. See [Backfilling Prior-Points Snapshots](#backfilling-prior-points-snapshots) |
- **`violations`** — full incident record including `prior_active_points` snapshot, `acknowledged_by` / `acknowledged_date`, and `amount` (financial amount in question for chargeback/repayment)
- **`violation_types`** — persisted custom violation type definitions added via the UI; `type_key` is prefixed `custom_` to prevent collisions with hardcoded keys
> An `active_cpas_scores` SQL view existed in earlier versions; it was dropped once scoring moved to the order-dependent clean-cycle model in `lib/rolloff.js`.
| 7 | Acknowledgment signature field | "Received by employee" name + date on the violation form; renders on the PDF replacing blank signature lines with recorded acknowledgment |
| 7 | Toast notification system | Global success/error/warning/info notifications for all user actions; auto-dismiss with progress bar; consistent dark theme |
| 7 | Department dropdown | Pre-loaded select on the violation form replacing free-text department input; shared `DEPARTMENTS` constant |
| 8 | Stakeholder demo page | Standalone `/demo` route with synthetic data; static HTML served before SPA catch-all; useful for non-live presentations |
| 9 | Custom violation types | Persisted user-defined violation types created from the form; `+ Add Type` / `Edit Type` UI; merged into the dropdown alongside hardcoded types; delete blocked when in use |
| 9 | Financial amount tracking | `amount` field on financial violations (chargeback, receipt negligence, custom types with the field enabled); stored on `violations`, rendered prominently on the PDF, amendable with audit-logged diffs |
| Column sort on dashboard | 🟢 | Click `Tier`, `Active Points`, or `Department` headers to sort in-place; one `useState` + comparator, no API changes |
| Department filter on dashboard | 🟢 | Multi-select dropdown to scope the employee table by department; `DEPARTMENTS` constant already exists |
| Keyboard shortcut: New Violation | 🟢 | `N` key triggers tab switch to the violation form; ~5 lines of code |
| Violation trend chart | 🟡 | Line/bar chart of violations per day/week/month, filterable by department or supervisor; useful for identifying systemic patterns |
| Department heat map | 🟡 | Grid view showing violation density and average CPAS score by department; helps supervisors identify team-level risk |
| Violation sparklines per employee | 🟡 | Tiny inline bar chart of points over the last 6 months in the employee modal |
| Supervisor scoped view | 🟡 | Dashboard filtered to a supervisor's direct reports, accessible via URL param (`?supervisor=Name`); no schema changes required |
| Employee photo / avatar | 🟢 | Optional avatar upload stored alongside the employee record; shown in the profile modal and dashboard row |
| Tier escalation alerts | 🟡 | Email or in-app notification when an employee crosses into Tier 2+ so the relevant supervisor is automatically informed |
| At-risk threshold config | 🟢 | Make the "at-risk" warning threshold (currently hardcoded at 2 pts) configurable per deployment via an env var |
| Expanded roles | 🟡 | Finer-grained access beyond the current `admin`/`user` split (e.g. supervisor-scoped or read-only roles) — builds on the shipped auth foundation |
*Proposed features are suggestions based on common HR documentation workflows. Priority and implementation order should be driven by actual operational needs.*