CPAS Violation Tracker
Single-container Dockerized web app for CPAS violation documentation and workforce standing management. Built with React + Vite (frontend), Node.js + Express (backend), SQLite (database), and Puppeteer (PDF generation).
© Jason Stedwell · git.alwisp.com/jason/cpas
The only requirement on your machine: Docker Desktop
Everything else — Node.js, npm, React build, Chromium for PDF — happens inside Docker.
Quickstart (Local)
# 1. Build the image (installs all deps + compiles React inside Docker)
docker build -t cpas .
# 2. Run it — set a strong admin password (the app requires login)
docker run -d --name cpas \
-p 3001:3001 \
-v cpas-data:/data \
-e ADMIN_USERNAME=admin \
-e ADMIN_PASSWORD='choose-a-strong-secret' \
cpas
# 3. Open http://localhost:3001 and sign in as admin
The app requires login. The admin account is created from
ADMIN_USERNAME/ADMIN_PASSWORDon every start. The image ships a placeholder password (changeme) that must be overridden. To rotate the admin password later, changeADMIN_PASSWORDand restart. Additional users are created in-app by an admin (Users button, top-right).
Update After Code Changes
docker build -t cpas .
docker stop cpas && docker rm cpas
docker run -d --name cpas -p 3001:3001 -v cpas-data:/data \
-e ADMIN_PASSWORD='choose-a-strong-secret' cpas
Deploying on Unraid
Step 1 — Build and export the image on your dev machine
docker build -t cpas:latest .
docker save cpas:latest | gzip > cpas-latest.tar.gz
Step 2 — Load the image on Unraid
Transfer cpas-latest.tar.gz to your Unraid server, then load it via the Unraid terminal:
docker load < /path/to/cpas-latest.tar.gz
Confirm the image is present:
docker images | grep cpas
Step 3 — Create the appdata directory
mkdir -p /mnt/user/appdata/cpas/db
Step 4 — Run the container
This is the verified working docker run command for Unraid (bridge networking with static IP):
docker run \
-d \
--name='cpas' \
--net='br0' \
--ip='10.2.0.14' \
--pids-limit 2048 \
-e TZ="America/Chicago" \
-e HOST_OS="Unraid" \
-e HOST_HOSTNAME="ALPHA" \
-e HOST_CONTAINERNAME="cpas" \
-e 'PORT'='3001' \
-e 'DB_PATH'='/data/cpas.db' \
-e 'ADMIN_USERNAME'='admin' \
-e 'ADMIN_PASSWORD'='choose-a-strong-secret' \
-l net.unraid.docker.managed=dockerman \
-l net.unraid.docker.webui='http://[IP]:[PORT:3001]' \
-v '/mnt/user/appdata/cpas/db':'/data':'rw' \
cpas:latest
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 |
DB_PATH |
/data/cpas.db |
SQLite database path inside the container |
ADMIN_USERNAME |
admin |
Bootstrap admin username (created/synced on startup) |
ADMIN_PASSWORD |
(strong secret) | Bootstrap admin password — must override the changeme default; re-syncs on every start |
| Volume | /mnt/user/appdata/cpas/db → /data |
Persists the database across container restarts and rebuilds |
Updating on Unraid
- Build and export the new image on your dev machine (Step 1 above)
- Load it on Unraid:
docker load < cpas-latest.tar.gz - Stop and remove the old container:
docker stop cpas && docker rm cpas - Re-run the
docker runcommand from Step 4 — the volume mount preserves all data
Note: The
--pids-limit 2048flag is critical. Without it, Chromium hits Unraid's default PID limit and PDF generation silently fails or crashes the container.
Unraid GUI alternative (Add Container form)
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:
| Field | Value |
|---|---|
| Name | cpas |
| Repository | cpas:latest (local image; leave Docker Hub URL blank) |
| Network Type | Bridge (or a custom br0 with a static IP) |
| WebUI | http://[IP]:[PORT:3001] |
| Port | Container 3001 → Host 3001 (TCP) |
| Path | Container /data → Host /mnt/user/appdata/cpas/db (Read/Write) |
| Variable | PORT = 3001 |
| Variable | DB_PATH = /data/cpas.db |
| Variable | ADMIN_USERNAME = admin |
| Variable | ADMIN_PASSWORD = (a strong secret — override the default) |
| Extra Parameters | --pids-limit 2048 (Advanced View → Extra Parameters) |
Verify
- Docker tab → cpas shows a green icon → click it → WebUI (or open
http://[IP]:3001). - A login screen appears — sign in with the
ADMIN_USERNAME/ADMIN_PASSWORDyou set. - Confirm ● API connected in the header.
- Health check (no login required):
http://[IP]:3001/api/health→{"status":"ok", ...}.
Troubleshooting
| Problem | Fix |
|---|---|
| Container won't start | Docker tab → container icon → Logs |
| Can't log in / forgot admin password | Edit Container → set a new ADMIN_PASSWORD → Restart. It re-syncs from this variable on every start. |
ADMIN_PASSWORD not set in logs |
Add the ADMIN_PASSWORD variable and restart — without it the admin account is not created. |
| PDF generation fails / container crashes under load | Confirm --pids-limit 2048 is set — Chromium spawns many processes per PDF. |
| Port 3001 conflict | Change the Host Port to 3002 in Edit Container. |
| "API unreachable" in UI | Confirm green icon, check Logs, try Restart. |
| DB permission error | Terminal: chmod 755 /mnt/user/appdata/cpas/db |
| Inspect DB directly | Terminal: docker exec -it cpas sh, then sqlite3 /data/cpas.db ".tables" |
Stakeholder Demo
A standalone demo page with synthetic data is available at /demo (e.g. http://localhost:3001/demo).
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.
Features
Authentication & User Accounts
- Login gate on the entire app — every page and every
/api/*route (except/api/healthand 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_PASSWORDenvironment 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
Company Dashboard
- Live table of all employees sorted by active CPAS points (highest risk first)
- Summary stat cards: total employees, elite standing (0 pts), with active points, at-risk count, highest active score
- At-risk badge: flags employees within 2 points of the next tier escalation
- Search/filter by name, department, or supervisor
- Click any employee name to open their full profile modal
- 📋 Audit Log button — filterable, paginated view of all system write actions
Violation Form
- Select existing employee or enter new employee by name
- Employee intelligence: shows current CPAS standing badge and 90-day violation count before submitting
- Violation type dropdown grouped by category; shows prior 90-day counts inline
- Custom violation types: add or edit user-defined types directly from the form (
+ Add Type/Edit Typebuttons); persisted to the database and merged into the dropdown alongside hardcoded types - Recidivist auto-escalation: if an employee has prior violations of the same type, points slider auto-sets to maximum per policy
- Repeat offense badge with prior count displayed
- Context-sensitive fields (time, minutes late, amount, location, description) shown only when relevant to violation type
- 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
- Tier crossing warning (TierWarning component): previews what tier the new points would push the employee into before submission
- Point slider for discretionary adjustments within the violation's min/max range
- 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
- One-click PDF download immediately after submission
- Toast notifications: success/error/warning feedback for form submissions, validation, and PDF downloads
Employee Profile Modal
- Full violation history with resolution status and amendment count badge per record
- ✎ Edit Employee button — update name, department, supervisor, or notes inline
- Merge Duplicate tab — reassign all violations from a duplicate record and delete it
- 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_pointssnapshot 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 asviolation_snapshots_recomputedwith reasonmanual_backfill. See Backfilling Prior-Points Snapshots below. - Toast notifications for all actions: negate, restore, delete, amend, PDF download, employee edit
Audit Log
- Append-only log of every write action: employee created/edited/merged, violation logged/amended/negated/restored/deleted
- Filterable by entity type (employee / violation) and action
- Paginated with load-more; accessible from the Dashboard toolbar
Violation Amendment
- Edit submitted violations' non-scoring fields without delete-and-resubmit
- Point values, violation type, and incident date are immutable
- Every change is stored as a field-level diff (old → new value) with timestamp and actor
In-App Documentation
- ? Docs button in the navbar opens a slide-in admin reference panel
- Covers feature map, CPAS tier system, workflow guidance, and roadmap
- No external link required; always reflects current deployed version
Toast Notification System
- Global toast notifications for all user actions across the application
- Four variants: success (green), error (red), warning (gold), info (blue)
- Auto-dismiss with configurable duration and visual progress bar countdown
- Slide-in animation; stacks up to 5 notifications simultaneously
- Consistent dark theme styling matching the rest of the UI
App Footer
- © Jason Stedwell copyright with auto-advancing year
- Live dev ticker: real-time elapsed counter since first commit (
2026-03-06), ticking every second inXd HHh MMm SSsformat with a pulsing green dot - Gitea repo link with icon — links directly to
git.alwisp.com/jason/cpas
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 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 (computeStanding).
PDF Generation
- Puppeteer + system Chromium (bundled in Docker image)
- Logo loaded from disk at startup (no hardcoded base64); falls back gracefully if not found
- Generated on-demand per violation via
GET /api/violations/:id/pdf - Filename:
CPAS_<EmployeeName>_<IncidentDate>.pdf - PDF captures prior active points at the time of the incident (snapshot stored on insert)
- Acknowledgment rendering: if the violation has an
acknowledged_byvalue, 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
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:
- Open the affected employee's profile modal.
- Click ↻ Backfill Snapshots next to the Active Violations header.
- Confirm the prompt. A toast reports
Updated X of Y snapshot(s)orSnapshots already up to date. - 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_pointsusing the current set of non-negated violations in the 90 days before itsincident_date. - Writes only the rows that actually changed and reports the diff.
- Runs inside a single transaction.
- Writes one
violation_snapshots_recomputedentry to the audit log withreason: "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.
API endpoint: POST /api/employees/:id/recompute-snapshots
Response shape:
{ "success": true, "scanned": 2, "updated": 1, "changes": [
{ "id": 47, "incident_date": "2026-04-02", "old": 0, "new": 3 }
]}
API Reference
All routes except GET /api/health and POST /api/auth/login require a valid Authorization: Bearer <token> header. Routes marked admin additionally require an admin role.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/health |
Health check (public; returns build SHA + timestamp) |
| POST | /api/auth/login |
Public — exchange username + password for a session token |
| GET | /api/auth/me |
Current session's user |
| POST | /api/auth/logout |
Invalidate the current session token |
| GET | /api/users |
admin — list user accounts |
| POST | /api/users |
admin — create a user (username, password ≥6 chars, role) |
| PATCH | /api/users/:id/password |
admin — reset a user's password |
| DELETE | /api/users/:id |
admin — delete a user (cannot delete self) |
| GET | /api/employees |
List all employees (includes notes) |
| GET | /api/employees/:id |
Single employee record |
| POST | /api/employees |
Create or upsert employee |
| PATCH | /api/employees/:id |
Edit name, department, supervisor, or notes |
| PATCH | /api/employees/:id/notes |
Save employee notes only (shorthand) |
| POST | /api/employees/:id/merge |
Merge duplicate employee; reassigns all violations |
| POST | /api/employees/:id/recompute-snapshots |
Manual backfill — rebuild prior_active_points on every violation for this employee. See Backfilling Prior-Points Snapshots |
| GET | /api/employees/:id/score |
Get active CPAS score for employee |
| GET | /api/employees/:id/expiration |
Active violation roll-off timeline with days remaining |
| GET | /api/employees/:id/violation-counts |
90-day non-negated counts grouped by violation type |
| GET | /api/employees/:id/violation-counts/alltime |
All-time non-negated counts + max points used per type |
| GET | /api/dashboard |
All employees with active points + violation counts |
| POST | /api/violations |
Log a new violation (accepts acknowledged_by, acknowledged_date, amount) |
| GET | /api/violations/employee/:id |
Violation history with resolutions + amendment counts |
| PATCH | /api/violations/:id/negate |
Negate a violation (soft delete + resolution record) |
| PATCH | /api/violations/:id/restore |
Restore a negated violation |
| PATCH | /api/violations/:id/amend |
Amend non-scoring fields with field-level diff logging |
| GET | /api/violations/:id/amendments |
Get amendment history for a violation |
| DELETE | /api/violations/:id |
Hard delete a violation |
| GET | /api/violations/:id/pdf |
Download violation PDF |
| GET | /api/violation-types |
List custom violation types |
| POST | /api/violation-types |
Create a custom violation type |
| PUT | /api/violation-types/:id |
Update a custom violation type |
| DELETE | /api/violation-types/:id |
Delete a custom violation type (blocked if any violation references it) |
| GET | /api/audit |
Paginated audit log (filterable by entity_type, entity_id) |
Project Structure
cpas/
├── Dockerfile # Multi-stage: builds React + runs Express w/ Chromium
├── .dockerignore
├── package.json # Backend (Express) deps
├── server.js # API + static file server + global auth guard
├── auth.js # scrypt hashing, sessions, bootstrap admin, auth middleware
├── lib/
│ └── rolloff.js # Clean-cycle point roll-off model (computeStanding)
├── db/
│ ├── schema.sql # Base table definitions (no views)
│ └── database.js # SQLite connection (better-sqlite3) + auto-migrations
├── pdf/
│ ├── generator.js # Puppeteer PDF generation
│ └── template.js # HTML template (loads logo from disk, ack signature rendering)
├── demo/ # Static stakeholder demo page (served at /demo)
└── client/ # React frontend (Vite)
├── package.json
├── vite.config.js
├── index.html
└── src/
├── main.jsx
├── App.jsx # Root app + AppFooter (copyright, dev ticker, Gitea link)
├── data/
│ ├── violations.js # All hardcoded CPAS violation definitions + groups
│ └── departments.js # DEPARTMENTS constant; single source of truth
├── hooks/
│ └── useEmployeeIntelligence.js # Score + history hook
├── styles/
│ └── mobile.css # Mobile breakpoint overrides only
└── components/
├── CpasBadge.jsx # Tier badge + color logic (canonical TIERS, getTier)
├── TierWarning.jsx # Pre-submit tier crossing alert
├── Dashboard.jsx # Company-wide leaderboard + audit log trigger
├── DashboardMobile.jsx # Mobile-optimized dashboard layout
├── ViolationForm.jsx # Violation entry form + ack signature + amount field
├── ViolationTypeModal.jsx # Create / edit / delete custom violation types
├── EmployeeModal.jsx # Employee profile + history modal
├── EditEmployeeModal.jsx # Employee edit + merge duplicate
├── AmendViolationModal.jsx # Non-scoring field amendment + diff history
├── AuditLog.jsx # Filterable audit log panel
├── NegateModal.jsx # Negate/resolve violation dialog
├── ViolationHistory.jsx # Violation list component
├── ExpirationTimeline.jsx # Per-violation 90-day roll-off countdown
├── EmployeeNotes.jsx # Inline notes editor with quick-add HR tags
├── ToastProvider.jsx # Global toast notification system + useToast hook
└── ReadmeModal.jsx # In-app admin documentation panel
Database Schema
Eight tables, no views. Active-point totals are computed in JavaScript by lib/rolloff.js, not in SQL:
employees— id, name, department, supervisor, notesviolations— full incident record includingprior_active_pointssnapshot,acknowledged_by/acknowledged_date, andamount(financial amount in question for chargeback/repayment)violation_resolutions— resolution type, details, resolved_by (linked to violations)violation_amendments— field-level diff log for violation edits; one row per changed field per amendmentviolation_types— persisted custom violation type definitions added via the UI;type_keyis prefixedcustom_to prevent collisions with hardcoded keysaudit_log— append-only record of every write action (action, entity_type, entity_id, performed_by, details, timestamp)users— auth accounts: username, scryptpassword_hash, role (admin/user)sessions— bearer-token login sessions: token, user_id, expires_at (7-day TTL)
An
active_cpas_scoresSQL view existed in earlier versions; it was dropped once scoring moved to the order-dependent clean-cycle model inlib/rolloff.js.
Amendable Fields
Point values, violation type, and incident date are immutable after submission. The following fields can be amended:
| Field | Notes |
|---|---|
incident_time |
Time of day the incident occurred |
location |
Where the incident took place |
details |
Narrative description |
submitted_by |
Supervisor who submitted |
witness_name |
Witness on record |
acknowledged_by |
Employee who acknowledged receipt |
acknowledged_date |
Date of employee acknowledgment |
amount |
Dollar amount in question (financial violations); typo-correctable for repayment records |
Roadmap
✅ Completed
| Phase | Feature | Description |
|---|---|---|
| 1 | Container scaffold | Docker multi-stage build, Express server, SQLite schema |
| 1 | Base violation form | Employee fields, violation type, incident date, point submission |
| 2 | Employee intelligence | Live CPAS standing badge and 90-day count shown before submitting |
| 2 | Prior violation highlighting | Violation dropdown annotates types with 90-day recurrence counts |
| 2 | Recidivist auto-escalation | Points slider auto-maximizes on repeat same-type violations |
| 2 | Violation history | Per-employee history list with resolution status |
| 3 | PDF generation | Puppeteer/Chromium PDF per violation, downloadable immediately post-submit |
| 3 | Prior-points snapshot | prior_active_points captured at insert time for accurate historical PDFs |
| 4 | Company dashboard | Sortable employee table with live tier badges and at-risk flags |
| 4 | Stat cards | Summary counts: total, clean, active, at-risk, highest score |
| 4 | Tier crossing warning | Pre-submit alert when new points push employee to next tier |
| 4 | Employee profile modal | Full history, negate/restore, hard delete, per-record PDF download |
| 4 | Negate & restore | Soft-delete violations with resolution type + notes, fully reversible |
| 5 | Employee edit / merge | Update employee name/dept/supervisor; merge duplicate records without losing history |
| 5 | Violation amendment | Edit non-scoring fields with field-level audit trail |
| 5 | Audit log | Append-only log of all system writes; filterable panel in the dashboard |
| 6 | Employee notes / flags | Free-text notes on employee record with quick-add HR tags; does not affect scoring |
| 6 | Point expiration timeline | Per-violation roll-off countdown with tier-drop projections |
| 6 | In-app documentation | Admin usage guide and feature map accessible from the navbar |
| 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 |
| 8 | App footer | Copyright (© Jason Stedwell), live dev ticker since first commit, Gitea repo icon+link |
| 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 |
| 10 | Authentication & user accounts | Login gate on all routes; scrypt-hashed passwords; 7-day bearer sessions; env-bootstrapped admin; admin-managed users with admin/user roles |
| 10 | Build version badge | Git SHA + build timestamp injected into version.json at docker build; surfaced in the footer and /api/health |
📋 Proposed
Effort ratings: 🟢 Low · 🟡 Medium · 🔴 High
Quick Wins (High value, low effort)
| Feature | Effort | Description |
|---|---|---|
| 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 |
Reporting & Analytics
| Feature | Effort | Description |
|---|---|---|
| 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 |
Employee Management
| Feature | Effort | Description |
|---|---|---|
| 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 |
Violation Workflow
| Feature | Effort | Description |
|---|---|---|
| Draft / pending violations | 🟡 | Save a violation as draft before finalizing; useful when incidents need review before being officially logged |
| Violation templates | 🟢 | Pre-fill the form with a saved violation type + common details for frequently logged incidents |
Notifications & Escalation
| Feature | Effort | Description |
|---|---|---|
| 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 |
Infrastructure & Ops
| Feature | Effort | Description |
|---|---|---|
| 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 |
| Automated DB backup | 🟡 | Cron job or Docker health hook to snapshot /data/cpas.db to a mounted backup volume or remote location on a schedule |
| Dark/light theme toggle | 🟡 | The UI is currently dark-only; a toggle would improve usability in bright environments |
Proposed features are suggestions based on common HR documentation workflows. Priority and implementation order should be driven by actual operational needs.