Docs consolidation + audit-log timezone fix + PTO-Exhausted sliding scale
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:
Jason Stedwell
2026-07-10 17:07:51 -05:00
parent cd0064ce5b
commit aef4ae60cb
11 changed files with 206 additions and 779 deletions
File diff suppressed because one or more lines are too long
+13 -13
View File
@@ -1,18 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CPAS Violation Tracker</title>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CPAS Violation Tracker</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; height: 100%; }
#root { height: 100%; }
</style>
<script type="module" crossorigin src="/assets/index-C1XYfYmx.js"></script>
</style>
<script type="module" crossorigin src="/assets/index-DhWLw3RT.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BoQf6yV_.css">
</head>
<body>
<div id="root"></div>
</body>
</head>
<body>
<div id="root"></div>
</body>
</html>
@@ -73,9 +73,13 @@ const s = {
},
};
// SQLite CURRENT_TIMESTAMP is UTC as "YYYY-MM-DD HH:MM:SS" with no zone marker;
// JS Date() would parse that space-separated form as local time and shift it by
// the viewer's UTC offset. Pin the bare SQLite form to UTC before formatting.
function fmtDt(iso) {
if (!iso) return '—';
return new Date(iso).toLocaleString('en-US', { timeZone: 'America/Chicago', dateStyle: 'medium', timeStyle: 'short' });
const utc = /^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d$/.test(iso) ? iso.replace(' ', 'T') + 'Z' : iso;
return new Date(utc).toLocaleString('en-US', { timeZone: 'America/Chicago', dateStyle: 'medium', timeStyle: 'short' });
}
export default function AmendViolationModal({ violation, onClose, onSaved }) {
+6 -1
View File
@@ -85,9 +85,14 @@ const s = {
},
};
// SQLite CURRENT_TIMESTAMP returns UTC as "YYYY-MM-DD HH:MM:SS" with no zone
// marker. JS Date() parses that space-separated form as LOCAL time, shifting
// every entry by the viewer's UTC offset (the ~5h drift seen from Chicago/CDT).
// Detect the bare SQLite form and pin it to UTC before formatting.
function fmtDt(iso) {
if (!iso) return '—';
return new Date(iso).toLocaleString('en-US', {
const utc = /^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d$/.test(iso) ? iso.replace(' ', 'T') + 'Z' : iso;
return new Date(utc).toLocaleString('en-US', {
timeZone: 'America/Chicago', dateStyle: 'medium', timeStyle: 'short',
});
}
+11 -5
View File
@@ -112,7 +112,13 @@ Roles: **Admin** can manage users plus everything a User can do; **User** can vi
## How Scoring Works
Every violation carries a **point value** set at the time of submission. Points count toward an employee's score only within a **rolling 90-day window** — once a violation is older than 90 days it automatically drops off and the score recalculates.
Every violation carries a **point value** set at the time of submission. Points do **not** each expire on their own 90-day timer. Instead, CPAS uses a **clean-cycle roll-off**:
- An employee's points start retiring only after they go a full **90 consecutive days with no new violation**.
- **Any new violation resets that 90-day clock** — the whole balance stays put and the countdown starts over.
- Each completed 90-day clean stretch removes **5 points, oldest violation first**. Dropping another 5 requires another full 90 clean days.
In short: staying clean is what burns points down, and a single new incident pushes everyone's roll-off back to square one.
Negated (voided) violations are excluded from scoring immediately. Hard-deleted violations are removed from the record entirely.
@@ -172,13 +178,13 @@ Shows current tier badge, active points, and 90-day violation count.
#### Notes & Flags
Free-text field for HR context (e.g. "On PIP", "Union member", "Pending investigation", "FMLA"). Quick-add tag buttons pre-fill common statuses. Notes are visible to anyone who opens the profile but **do not affect CPAS scoring**. Edit inline; saves on blur.
#### Point Expiration Timeline
Visible when the employee has active points. Shows each active violation as a progress bar indicating how far through its 90-day window it is, days remaining until roll-off, and a **tier-drop indicator** for violations whose expiration would move the employee down a tier.
#### Point Roll-Off Timeline
Visible when the employee has active points. Lists each future roll-off event (Roll-off #1, #2, …) — how many points retire, a progress bar and days-remaining countdown to that event, the projected date, and a **tier-drop indicator** where a roll-off would move the employee down a tier. It reflects the clean-cycle model: the countdown is anchored to the last violation date, and logging a new violation resets it.
#### Violation History
Full record of all submissions — active, negated, and resolved.
- **Amend** — edit non-scoring fields (location, details, witness, submitted-by, incident time, acknowledged-by, acknowledged-date) on any active violation. Every change is logged as a field-level diff (old → new) with timestamp. Points, type, and incident date are immutable.
- **Amend** — edit non-scoring fields (location, details, witness, submitted-by, incident time, acknowledged-by, acknowledged-date, amount) on any active violation. Every change is logged as a field-level diff (old → new) with timestamp. Points, type, and incident date are immutable.
- **Negate** — soft-delete a violation with a resolution type and notes. The record is preserved in history; the points are immediately removed from the score. Fully reversible via **Restore**.
- **Hard delete** — permanent removal. Use only for genuine data entry errors.
- **PDF** — download the formal violation document for any historical record. If the violation has an employee acknowledgment on record, the PDF shows the filled-in name and date instead of blank signature lines.
@@ -234,7 +240,7 @@ The audit log is the authoritative record for compliance review. Nothing in it c
Amendments allow corrections to a violation's non-scoring fields without deleting and re-submitting, which would disrupt the audit trail and the prior-points snapshot.
**Amendable fields:** incident time, location, details, submitted-by, witness name, acknowledged-by, acknowledged-date.
**Amendable fields:** incident time, location, details, submitted-by, witness name, acknowledged-by, acknowledged-date, amount (dollar figure on financial violations).
**Immutable fields:** violation type, incident date, point value.
+1 -1
View File
@@ -19,7 +19,7 @@ export const violationData = {
},
pto_exhausted: {
name: 'Absence - PTO Exhausted', category: 'Attendance & Punctuality',
minPoints: 5, maxPoints: 5, chapter: 'Chapter 4, Section 5',
minPoints: 1, maxPoints: 5, chapter: 'Chapter 4, Section 5',
fields: ['description'],
description: 'Any absence after PTO bank reaches zero'
},