Docs consolidation + audit-log timezone fix + PTO-Exhausted sliding scale
Build and Push Docker Image / build (push) Successful in 49s
Build and Push Docker Image / build (push) Successful in 49s
Documentation: - Consolidate 6 doc surfaces to 3. Merge Unraid guide into README and fold durable mobile rules into AGENTS.md; remove README_UNRAID_INSTALL.md, MOBILE_RESPONSIVE.md, and the misplaced "CPAS Violation Tracker.md" vault note. - Fix drift across README/AGENTS/in-app guide: clean-cycle roll-off model (not per-violation 90-day window), auth documented as shipped, dropped active_cpas_scores view, correct table count (8 tables/0 views), auth endpoints + ADMIN_PASSWORD in run commands, roadmap updated. Fixes: - Audit log / amendment timestamps: SQLite CURRENT_TIMESTAMP is UTC with no zone marker; new Date() parsed the space-separated form as local time, shifting entries +5h in Chicago/CDT. Pin the bare form to UTC before formatting in AuditLog.jsx and AmendViolationModal.jsx. - Absence - PTO Exhausted: fixed 5 pts -> sliding scale 1-5. Rebuild client bundle to include the above. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -19,22 +19,26 @@ Everything else — Node.js, npm, React build, Chromium for PDF — happens insi
|
||||
# 1. Build the image (installs all deps + compiles React inside Docker)
|
||||
docker build -t cpas .
|
||||
|
||||
# 2. Run it
|
||||
# 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
|
||||
# 3. Open http://localhost:3001 and sign in as admin
|
||||
```
|
||||
|
||||
> **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).
|
||||
|
||||
## Update After Code Changes
|
||||
|
||||
```bash
|
||||
docker build -t cpas .
|
||||
docker stop cpas && docker rm cpas
|
||||
docker run -d --name cpas -p 3001:3001 -v cpas-data:/data cpas
|
||||
docker run -d --name cpas -p 3001:3001 -v cpas-data:/data \
|
||||
-e ADMIN_PASSWORD='choose-a-strong-secret' cpas
|
||||
```
|
||||
|
||||
---
|
||||
@@ -85,12 +89,16 @@ docker run \
|
||||
-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
|
||||
@@ -102,6 +110,8 @@ Access the app at `http://10.2.0.14:3001` (or whatever static IP you assigned).
|
||||
| `--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
|
||||
@@ -113,18 +123,62 @@ Access the app at `http://10.2.0.14:3001` (or whatever static IP you assigned).
|
||||
|
||||
> **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.
|
||||
|
||||
### 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
|
||||
|
||||
1. Docker tab → **cpas** shows a green icon → click it → **WebUI** (or open `http://[IP]:3001`).
|
||||
2. A **login screen** appears — sign in with the `ADMIN_USERNAME` / `ADMIN_PASSWORD` you set.
|
||||
3. Confirm **● API connected** in the header.
|
||||
4. 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 served as a static route before the SPA catch-all and requires no authentication.
|
||||
Useful for showing the app to stakeholders without exposing live employee data.
|
||||
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/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
|
||||
|
||||
### 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
|
||||
@@ -157,7 +211,7 @@ Useful for showing the app to stakeholders without exposing live employee data.
|
||||
- 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 Expiration Timeline** — shows when each active violation rolls off the 90-day window, with a progress bar, days-remaining countdown, and projected tier-drop indicators
|
||||
- **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.
|
||||
- **Toast notifications** for all actions: negate, restore, delete, amend, PDF download, employee edit
|
||||
|
||||
@@ -200,7 +254,7 @@ Useful for showing the app to stakeholders without exposing live employee data.
|
||||
| 25–29 | 5 | Final Decision |
|
||||
| 30+ | 6 | Separation |
|
||||
|
||||
Scores are computed over a **rolling 90-day window** (negated violations excluded).
|
||||
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`).
|
||||
|
||||
### PDF Generation
|
||||
- Puppeteer + system Chromium (bundled in Docker image)
|
||||
@@ -253,9 +307,18 @@ Response shape:
|
||||
|
||||
## 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 (returns build SHA + timestamp) |
|
||||
| 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 |
|
||||
@@ -291,9 +354,12 @@ cpas/
|
||||
├── Dockerfile # Multi-stage: builds React + runs Express w/ Chromium
|
||||
├── .dockerignore
|
||||
├── package.json # Backend (Express) deps
|
||||
├── server.js # API + static file server
|
||||
├── 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 # Tables + 90-day active score view
|
||||
│ ├── schema.sql # Base table definitions (no views)
|
||||
│ └── database.js # SQLite connection (better-sqlite3) + auto-migrations
|
||||
├── pdf/
|
||||
│ ├── generator.js # Puppeteer PDF generation
|
||||
@@ -336,7 +402,7 @@ cpas/
|
||||
|
||||
## Database Schema
|
||||
|
||||
Six tables + one view:
|
||||
Eight tables, no views. Active-point totals are computed in JavaScript by [`lib/rolloff.js`](lib/rolloff.js), not in SQL:
|
||||
|
||||
- **`employees`** — id, name, department, supervisor, notes
|
||||
- **`violations`** — full incident record including `prior_active_points` snapshot, `acknowledged_by` / `acknowledged_date`, and `amount` (financial amount in question for chargeback/repayment)
|
||||
@@ -344,7 +410,10 @@ Six tables + one view:
|
||||
- **`violation_amendments`** — field-level diff log for violation edits; one row per changed field per amendment
|
||||
- **`violation_types`** — persisted custom violation type definitions added via the UI; `type_key` is prefixed `custom_` to prevent collisions with hardcoded keys
|
||||
- **`audit_log`** — append-only record of every write action (action, entity_type, entity_id, performed_by, details, timestamp)
|
||||
- **`active_cpas_scores`** (view) — sum of points for non-negated violations in rolling 90 days, grouped by employee
|
||||
- **`users`** — auth accounts: username, scrypt `password_hash`, role (`admin`/`user`)
|
||||
- **`sessions`** — bearer-token login sessions: token, user_id, expires_at (7-day TTL)
|
||||
|
||||
> 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`.
|
||||
|
||||
---
|
||||
|
||||
@@ -397,6 +466,8 @@ Point values, violation type, and incident date are **immutable** after submissi
|
||||
| 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` |
|
||||
|
||||
---
|
||||
|
||||
@@ -440,13 +511,12 @@ Effort ratings: 🟢 Low · 🟡 Medium · 🔴 High
|
||||
|---------|--------|-------------|
|
||||
| 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 |
|
||||
| version.json / build badge | 🟢 | Inject git SHA + build timestamp into a static file during `docker build`; surfaced in the footer and `/api/health` |
|
||||
|
||||
#### Infrastructure & Ops
|
||||
|
||||
| Feature | Effort | Description |
|
||||
|---------|--------|-------------|
|
||||
| Multi-user auth | 🔴 | Simple login with role-based access (admin, supervisor, read-only); currently the app runs on a trusted internal network with no auth |
|
||||
| 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 |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user