From 979e9724e002f0b180a8314fa065eec50fcc66bd Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 7 Mar 2026 22:01:35 -0600 Subject: [PATCH 01/40] fix: score endpoint now returns total_violations and negated_count The /api/employees/:id/score endpoint previously only returned data from the active_cpas_scores view (active_points + violation_count for the 90-day window). The EmployeeModal score cards reference total_violations and negated_count which were undefined, causing blank displays. Now queries the violations table directly for all-time totals alongside the rolling 90-day active data. --- server.js | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/server.js b/server.js index 59ed7d3..f4c59cc 100755 --- a/server.js +++ b/server.js @@ -128,10 +128,29 @@ app.patch('/api/employees/:id/notes', (req, res) => { res.json({ id, notes: newNotes }); }); -// Employee score (current snapshot) +// Employee score (current snapshot) — includes total violations + negated count app.get('/api/employees/:id/score', (req, res) => { - const row = db.prepare('SELECT * FROM active_cpas_scores WHERE employee_id = ?').get(req.params.id); - res.json(row || { employee_id: req.params.id, active_points: 0, violation_count: 0 }); + const empId = req.params.id; + + // Active points from the 90-day rolling view + const active = db.prepare('SELECT * FROM active_cpas_scores WHERE employee_id = ?').get(empId); + + // Total violations (all time) and negated count + const totals = db.prepare(` + SELECT + COUNT(*) AS total_violations, + COALESCE(SUM(negated), 0) AS negated_count + FROM violations + WHERE employee_id = ? + `).get(empId); + + res.json({ + employee_id: empId, + active_points: active ? active.active_points : 0, + violation_count: active ? active.violation_count : 0, + total_violations: totals ? totals.total_violations : 0, + negated_count: totals ? totals.negated_count : 0, + }); }); // ── Expiration Timeline ────────────────────────────────────────────────────── -- 2.52.0 From 5d835e6b911cf4f1e6edac2bf6a93d6d8cc1f3b4 Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 7 Mar 2026 23:13:26 -0600 Subject: [PATCH 02/40] feat: add shared DEPARTMENTS constant --- client/src/data/departments.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 client/src/data/departments.js diff --git a/client/src/data/departments.js b/client/src/data/departments.js new file mode 100644 index 0000000..9ed3cfa --- /dev/null +++ b/client/src/data/departments.js @@ -0,0 +1,9 @@ +export const DEPARTMENTS = [ + 'Administrative', + 'Business Development', + 'Design and Content', + 'Executive', + 'Implementation and Support', + 'Operations', + 'Production', +]; -- 2.52.0 From 0f31677631a959ea55178c4b3f03a34578fc0aab Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 7 Mar 2026 23:14:40 -0600 Subject: [PATCH 03/40] feat: replace department text input with preloaded select dropdown --- client/src/components/ViolationForm.jsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/client/src/components/ViolationForm.jsx b/client/src/components/ViolationForm.jsx index 3367803..2ca944b 100755 --- a/client/src/components/ViolationForm.jsx +++ b/client/src/components/ViolationForm.jsx @@ -6,6 +6,7 @@ import CpasBadge from './CpasBadge'; import TierWarning from './TierWarning'; import ViolationHistory from './ViolationHistory'; import { useToast } from './ToastProvider'; +import { DEPARTMENTS } from '../data/departments'; const s = { content: { padding: '32px 40px', background: '#111217', borderRadius: '10px', color: '#f8f9fa' }, @@ -171,12 +172,21 @@ export default function ViolationForm() { )}
- {[['employeeName','Employee Name','text','John Doe'],['department','Department','text','Engineering'],['supervisor','Supervisor Name','text','Jane Smith'],['witnessName','Witness Name (Officer)','text','Officer Name']].map(([name,label,type,ph]) => ( + {[['employeeName','Employee Name','John Doe'],['supervisor','Supervisor Name','Jane Smith'],['witnessName','Witness Name (Officer)','Officer Name']].map(([name,label,ph]) => (
- +
))} +
+ + +
-- 2.52.0 From d8793000fc69fa2089318fea2fd764e52c758fef Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 7 Mar 2026 23:15:15 -0600 Subject: [PATCH 04/40] feat: replace department text input with preloaded select dropdown --- client/src/components/EditEmployeeModal.jsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/src/components/EditEmployeeModal.jsx b/client/src/components/EditEmployeeModal.jsx index a438e1b..1d71ed4 100644 --- a/client/src/components/EditEmployeeModal.jsx +++ b/client/src/components/EditEmployeeModal.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import axios from 'axios'; +import { DEPARTMENTS } from '../data/departments'; const s = { overlay: { @@ -133,7 +134,12 @@ export default function EditEmployeeModal({ employee, onClose, onSaved }) {
Full Name
setName(e.target.value)} />
Department
- setDepartment(e.target.value)} placeholder="Optional" /> +
Supervisor
setSupervisor(e.target.value)} placeholder="Optional" />
-- 2.52.0 From 76f972562b376e0b64072fb0e0264c3229988d46 Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 7 Mar 2026 23:27:25 -0600 Subject: [PATCH 05/40] feat: add stakeholder demo landing page with synthetic data --- demo/index.html | 750 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 750 insertions(+) create mode 100644 demo/index.html diff --git a/demo/index.html b/demo/index.html new file mode 100644 index 0000000..50f4c60 --- /dev/null +++ b/demo/index.html @@ -0,0 +1,750 @@ + + + + + +CPAS Tracker — Demo Preview + + + + + + + +
+
+ DEMO ENVIRONMENT — Simulated data for stakeholder preview only — Not connected to live database +
+
+ + + + +
+ + +
+ +
+
Corrective Performance Action System
+

Employee Compliance Dashboard

+

Real-time visibility into workforce disciplinary standing. Track violations, monitor tier escalations, and generate signed documentation — all in one place.

+
+
47
Total Employees
+
+
23
Active Violations (90d)
+
+
3
At-Risk (Tier 3+)
+
+
91%
In Good Standing
+
+
+ +
+
+
New This Week
+
6
+
+2 vs prior week
+
+
+
+
Tier 3+ Employees
+
3
+
Requires attention
+
+
+
+
PDFs Generated
+
18
+
This month
+
+
+
+
Expiring (30d)
+
9
+
Points rolling off
+
+
+
+ +
+ +
+
+ Employee Roster + 47 total +
+ + + + + + + + + + + + +
EmployeeDeptStanding
Marcus T.
Operations
22 pts — Tier 4
Janelle R.
Production
17 pts — Tier 3
Devon H.
Operations
15 pts — Tier 3
Priya S.
Impl & Support
12 pts — Tier 2
Carlos M.
Production
7 pts — Tier 1
Aisha W.
Administrative
5 pts — Tier 1
Tom B.
Design & Content
2 pts — Elite
Sandra K.
Business Dev
0 pts — Elite
+
+ + +
+
+ Recent Violations + Last 7 days +
+
+
+
+
+
Marcus T. ★ REPEAT
+
Unauthorized Absence — Operations
+
Mar 6·D. Williams
+
+
+5
+
+
+
+
+
Janelle R.
+
Insubordination — Production
+
Mar 5·K. Thompson
+
+
+4
+
+
+
+
+
Devon H.
+
Tardiness (3×) — Operations
+
Mar 4·D. Williams
+
+
+3
+
+
+
+
+
Carlos M.
+
Cell Phone Policy — Production
+
Mar 3·K. Thompson
+
+
+2
+
+
+
+
+
Priya S.
+
Dress Code Violation — Impl & Support
+
Mar 2·M. Johnson
+
+
+1
+
+
+
+
+
Aisha W.
+
Late Return from Break — Administrative
+
Mar 1·S. Martinez
+
+
+1
+
+
+
+
+ +
+ +
+
+ Violations by Department + 90-day window +
+
+
Operations
8
+
Production
6
+
Impl & Support
4
+
Administrative
2
+
Business Dev
1
+
Design & Content
1
+
Executive
0
+
+
+ + +
+
+ Audit Log + System activity +
+
+
03/06 2:14p
Violation #41 created — Marcus T.
+5 pts
+
03/06 2:15p
PDF generated for Violation #41
+
03/05 9:40a
Violation #40 created — Janelle R.
+4 pts
+
03/04 11:20a
Employee Devon H. record updated
+
03/04 8:55a
Violation #39 created — Devon H.
+3 pts
+
03/03 3:30p
Violation #38 amended — Carlos M.
−1 pt
+
03/02 1:05p
Duplicate record merged — R. Johnson
+
+
+
+ + +
+
+
CPAS Tier Scale
+
+
+
0–4
Elite
Standing
+
5–9
Tier 1
Realignment
+
10–14
Tier 2
Admin Lockdown
+
15–19
Tier 3
Verification
+
20–24
Tier 4
Risk Mitigation
+
25–29
Tier 5
Final Decision
+
30+
Tier 6
Separation
+
+
+ + +
System Capabilities
+
+
Repeat Offense Detection
Automatically flags prior violations for the same type and escalates point recommendations per recidivist policy.
+
📄
One-Click PDF Generation
Generates signed, professional violation documents instantly — with or without employee acknowledgment signatures.
+
🔀
Duplicate Record Merge
Consolidate duplicate employee records while preserving full violation history under the canonical profile.
+
📊
90-Day Rolling Window
Points automatically expire after 90 days. Active standing reflects only the current compliance window.
+
🏷️
Tier Escalation Warnings
Real-time alerts when a new violation would push an employee across a tier boundary before you submit.
+
🗂️
Full Audit Trail
Every create, amendment, merge, and PDF generation is logged with timestamp and operator attribution.
+
+ +
+ + +
+ +
+
+ ⚠ DEMO VIEW — Form fields shown with sample data. Submission is disabled in demo mode. +
+
+ +
+
+
Employee Information
+
+
Quick-Select Existing Employee:
+
+ Marcus Thompson — Operations +
+
+ Current Standing: + 22 pts — Tier 4 · Risk Mitigation + 4 violations in last 90 days +
+
+
+
Employee Name:
Marcus Thompson
+
Department:
Operations
+
Supervisor Name:
D. Williams
+
Witness Name (Officer):
Officer Name
+
+
+ +
+
Violation Details
+
+
+
Violation Type:
+
+ Unauthorized Absence ★ 2x in 90 days +
+
+ Unauthorized Absence ★ Repeat — 2x prior
+ Absence from scheduled work without prior approval or acceptable documentation.
+ Chapter 4, Section 4.1 — Attendance & Punctuality +
+
+ Repeat offense detected. Point slider set to maximum (5 pts) per recidivist policy. Adjust if needed. +
+
+
Incident Date:
2026-03-06
+
+
+ ⚠ Tier escalation warning: Adding 5 pts will bring Marcus to 27 pts (Tier 5 — Final Decision). This is one tier below Separation. Review carefully. +
+
+
CPAS Point Assessment
+
Unauthorized Absence: 3–5 Points
+
+
5 Points
+
Adjust to reflect severity and context
+
+
+ +
+
Employee Acknowledgment
+
If the employee is present and acknowledges receipt of this violation, enter their name and the date below.
+
+
Acknowledged By:
Employee's printed name
+
Acknowledgment Date:
yyyy-mm-dd
+
+
+ +
+ + +
+
+ +
+ +
+ +
+ +
DEMO BUILD — All data synthetic — Not for operational use
+
© 2026 MPM
+
+ + + + -- 2.52.0 From 7ef00796bd4854acfe2b3e2984a203595fcd00a2 Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 7 Mar 2026 23:59:00 -0600 Subject: [PATCH 06/40] feat: serve demo/ folder as static route at /demo Adds express.static middleware for the demo/ directory mounted at /demo, placed before the SPA catch-all so /demo/index.html resolves to the standalone stakeholder demo page instead of the React app. --- server.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server.js b/server.js index f4c59cc..d1dc66b 100755 --- a/server.js +++ b/server.js @@ -11,6 +11,11 @@ app.use(cors()); app.use(express.json()); app.use(express.static(path.join(__dirname, 'client', 'dist'))); +// ── Demo static route ───────────────────────────────────────────────────────── +// Serves the standalone stakeholder demo page at /demo/index.html +// Must be registered before the SPA catch-all below. +app.use('/demo', express.static(path.join(__dirname, 'demo'))); + // ── Audit helper ───────────────────────────────────────────────────────────── function audit(action, entityType, entityId, performedBy, details) { try { -- 2.52.0 From 575c4c57fd663b732702189761f6615cf4796a6d Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 8 Mar 2026 00:11:18 -0600 Subject: [PATCH 07/40] fix: add demo/ folder to Dockerfile COPY so /demo route is served in container --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index c3bebec..18091ea 100755 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,7 @@ COPY server.js ./ COPY package.json ./ COPY db/ ./db/ COPY pdf/ ./pdf/ +COPY demo/ ./demo/ COPY client/public/static ./client/dist/static RUN mkdir -p /data EXPOSE 3001 -- 2.52.0 From 232814db93adae0e78c1acbc725351b6a19468f9 Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 8 Mar 2026 00:19:59 -0600 Subject: [PATCH 08/40] feat: enhance demo footer with copyright, Gitea link, and live dev-time ticker --- demo/index.html | 209 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 176 insertions(+), 33 deletions(-) diff --git a/demo/index.html b/demo/index.html index 50f4c60..e43ff4a 100644 --- a/demo/index.html +++ b/demo/index.html @@ -10,27 +10,27 @@ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } :root { - --gold: #d4af37; - --gold-lt: #ffdf8a; - --gold-dk: #a88520; - --bg: #050608; - --bg-nav: #000000; - --bg-card: #111217; - --bg-section:#181924; - --border: #222; - --border-lt: #2a2b3a; - --text: #f8f9fa; - --text-muted:#9ca0b8; - --text-dim: #d1d3e0; - --green: #28a745; - --green-bg: #d4edda; - --yellow: #856404; - --yellow-bg: #fff3cd; - --red: #d9534f; - --red-bg: #f8d7da; - --red-dk: #721c24; - --red-dk-bg: #f5c6cb; - --sep: #721c24; + --gold: #d4af37; + --gold-lt: #ffdf8a; + --gold-dk: #a88520; + --bg: #050608; + --bg-nav: #000000; + --bg-card: #111217; + --bg-section: #181924; + --border: #222; + --border-lt: #2a2b3a; + --text: #f8f9fa; + --text-muted: #9ca0b8; + --text-dim: #d1d3e0; + --green: #28a745; + --green-bg: #d4edda; + --yellow: #856404; + --yellow-bg: #fff3cd; + --red: #d9534f; + --red-bg: #f8d7da; + --red-dk: #721c24; + --red-dk-bg: #f5c6cb; + --sep: #721c24; } html { scroll-behavior: smooth; } @@ -407,8 +407,94 @@ .feature-desc { font-size: 12px; color: var(--text-muted); line-height: 1.6; } /* ── FOOTER ── */ - footer { border-top: 1px solid var(--border); padding: 24px 40px; display: flex; align-items: center; justify-content: space-between; font-size: 11px; color: var(--text-muted); font-family: 'DM Mono', monospace; } - .footer-brand { font-family: 'Syne', sans-serif; font-size: 13px; font-weight: 700; color: var(--text-dim); } + footer { + border-top: 1px solid var(--border); + padding: 20px 40px; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; + font-size: 11px; + color: var(--text-muted); + font-family: 'DM Mono', monospace; + background: var(--bg-nav); + } + .footer-left { + display: flex; + align-items: center; + gap: 18px; + } + .footer-brand { + font-family: 'Syne', sans-serif; + font-size: 13px; + font-weight: 700; + color: var(--text-dim); + } + .footer-copy { + color: var(--text-muted); + font-size: 11px; + } + .footer-gitea { + display: flex; + align-items: center; + gap: 6px; + color: var(--text-muted); + text-decoration: none; + padding: 4px 10px; + border: 1px solid var(--border-lt); + border-radius: 5px; + transition: border-color 0.2s, color 0.2s; + font-size: 11px; + } + .footer-gitea:hover { + border-color: var(--gold-dk); + color: var(--gold-lt); + } + .footer-gitea svg { + width: 14px; + height: 14px; + fill: currentColor; + flex-shrink: 0; + } + .footer-right { + display: flex; + align-items: center; + gap: 18px; + } + .footer-ticker { + display: flex; + align-items: center; + gap: 8px; + background: rgba(212,175,55,0.06); + border: 1px solid rgba(212,175,55,0.2); + border-radius: 5px; + padding: 4px 12px; + } + .footer-ticker-label { + font-size: 10px; + color: var(--text-muted); + letter-spacing: 0.5px; + text-transform: uppercase; + } + .footer-ticker-time { + font-family: 'DM Mono', monospace; + font-size: 12px; + color: var(--gold); + font-weight: 500; + letter-spacing: 1px; + } + .footer-ticker-dot { + width: 5px; height: 5px; border-radius: 50%; + background: var(--gold); + animation: pulse 2s infinite; + flex-shrink: 0; + } + .footer-divider { + width: 1px; + height: 16px; + background: var(--border-lt); + } /* ── ANIMATIONS ── */ .fade-in { opacity: 0; transform: translateY(16px); animation: fadeUp 0.5s ease forwards; } @@ -429,6 +515,8 @@ .hero h1 { font-size: 26px; } nav { padding: 0 16px; } .main { padding: 0 12px 60px; } + footer { padding: 16px 20px; flex-direction: column; align-items: flex-start; } + .footer-right { flex-wrap: wrap; } } @@ -531,7 +619,7 @@
-
Marcus T. ★ REPEAT
+
Marcus T. ☆ REPEAT
Unauthorized Absence — Operations
Mar 6·D. Williams
@@ -643,8 +731,8 @@
Repeat Offense Detection
Automatically flags prior violations for the same type and escalates point recommendations per recidivist policy.
📄
One-Click PDF Generation
Generates signed, professional violation documents instantly — with or without employee acknowledgment signatures.
-
🔀
Duplicate Record Merge
Consolidate duplicate employee records while preserving full violation history under the canonical profile.
-
📊
90-Day Rolling Window
Points automatically expire after 90 days. Active standing reflects only the current compliance window.
+
📀
Duplicate Record Merge
Consolidate duplicate employee records while preserving full violation history under the canonical profile.
+
🕊
90-Day Rolling Window
Points automatically expire after 90 days. Active standing reflects only the current compliance window.
🏷️
Tier Escalation Warnings
Real-time alerts when a new violation would push an employee across a tier boundary before you submit.
🗂️
Full Audit Trail
Every create, amendment, merge, and PDF generation is logged with timestamp and operator attribution.
@@ -656,7 +744,7 @@
- ⚠ DEMO VIEW — Form fields shown with sample data. Submission is disabled in demo mode. + ⚡ DEMO VIEW — Form fields shown with sample data. Submission is disabled in demo mode.
@@ -688,10 +776,10 @@
Violation Type:
- Unauthorized Absence ★ 2x in 90 days + Unauthorized Absence ☆ 2x in 90 days
- Unauthorized Absence ★ Repeat — 2x prior
+ Unauthorized Absence ☆ Repeat — 2x prior
Absence from scheduled work without prior approval or acceptable documentation.
Chapter 4, Section 4.1 — Attendance & Punctuality
@@ -702,7 +790,7 @@
Incident Date:
2026-03-06
- ⚠ Tier escalation warning: Adding 5 pts will bring Marcus to 27 pts (Tier 5 — Final Decision). This is one tier below Separation. Review carefully. + ⚡ Tier escalation warning: Adding 5 pts will bring Marcus to 27 pts (Tier 5 — Final Decision). This is one tier below Separation. Review carefully.
CPAS Point Assessment
@@ -733,9 +821,28 @@
- -
DEMO BUILD — All data synthetic — Not for operational use
-
© 2026 MPM
+ +
-- 2.52.0 From 7e1164af13d93379a2ce8de9dae6103b16a8bf07 Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 8 Mar 2026 00:21:30 -0600 Subject: [PATCH 09/40] feat: add footer with copyright, Gitea repo link, and live dev ticker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - © Jason Stedwell copyright with current year - Gitea icon + link to https://git.alwisp.com/jason/cpas - Running elapsed time ticker since first commit (2026-03-06T11:33:32) ticks every second: Xd HHh MMm SSs format - App layout changed to flex column so footer pins to page bottom - Footer styles isolated in `sf` object for clarity --- client/src/App.jsx | 77 +--------------------------------------------- 1 file changed, 1 insertion(+), 76 deletions(-) diff --git a/client/src/App.jsx b/client/src/App.jsx index b3645c2..df8dadf 100755 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,76 +1 @@ -import React, { useState } from 'react'; -import ViolationForm from './components/ViolationForm'; -import Dashboard from './components/Dashboard'; -import ReadmeModal from './components/ReadmeModal'; -import ToastProvider from './components/ToastProvider'; - -const tabs = [ - { id: 'dashboard', label: '📊 Dashboard' }, - { id: 'violation', label: '+ New Violation' }, -]; - -const s = { - app: { minHeight: '100vh', background: '#050608', fontFamily: "'Segoe UI', Arial, sans-serif", color: '#f8f9fa' }, - nav: { background: '#000000', padding: '0 40px', display: 'flex', alignItems: 'center', gap: 0, borderBottom: '1px solid #333' }, - logoWrap: { display: 'flex', alignItems: 'center', marginRight: '32px', padding: '14px 0' }, - logoImg: { height: '28px', marginRight: '10px' }, - logoText: { color: '#f8f9fa', fontWeight: 800, fontSize: '18px', letterSpacing: '0.5px' }, - tab: (active) => ({ - padding: '18px 22px', - color: active ? '#f8f9fa' : 'rgba(248,249,250,0.6)', - borderBottom: active ? '3px solid #d4af37' : '3px solid transparent', - cursor: 'pointer', fontWeight: active ? 700 : 400, fontSize: '14px', - background: 'none', border: 'none', - }), - // Docs button sits flush-right in the nav - docsBtn: { - marginLeft: 'auto', - background: 'none', - border: '1px solid #2a2b3a', - color: '#9ca0b8', - borderRadius: '6px', - padding: '6px 14px', - fontSize: '12px', - cursor: 'pointer', - fontWeight: 600, - letterSpacing: '0.3px', - display: 'flex', - alignItems: 'center', - gap: '6px', - }, - card: { maxWidth: '1100px', margin: '30px auto', background: '#111217', borderRadius: '10px', boxShadow: '0 2px 16px rgba(0,0,0,0.6)', border: '1px solid #222' }, -}; - -export default function App() { - const [tab, setTab] = useState('dashboard'); - const [showReadme, setShowReadme] = useState(false); - - return ( - -
- - -
- {tab === 'dashboard' ? : } -
- - {showReadme && setShowReadme(false)} />} -
-
- ); -} +aW1wb3J0IFJlYWN0LCB7IHVzZVN0YXRlLCB1c2VFZmZlY3QgfSBmcm9tICdyZWFjdCc7CmltcG9ydCBWaW9sYXRpb25Gb3JtIGZyb20gJy4vY29tcG9uZW50cy9WaW9sYXRpb25Gb3JtJzsKaW1wb3J0IERhc2hib2FyZCAgICAgZnJvbSAnLi9jb21wb25lbnRzL0Rhc2hib2FyZCc7CmltcG9ydCBSZWFkbWVNb2RhbCAgIGZyb20gJy4vY29tcG9uZW50cy9SZWFkbWVNb2RhbCc7CmltcG9ydCBUb2FzdFByb3ZpZGVyIGZyb20gJy4vY29tcG9uZW50cy9Ub2FzdFByb3ZpZGVyJzsKCmNvbnN0IFJFUE9fVVJMICAgICAgPSAnaHR0cHM6Ly9naXQuYWx3aXNwLmNvbS9qYXNvbi9jcGFzJzsKLy8gRmlyc3QgY29tbWl0IHRpbWVzdGFtcCDigJQgdXNlZCB0byBkcml2ZSB0aGUgZGV2IHRpY2tlcgpjb25zdCBQUk9KRUNUX1NUQVJUID0gbmV3IERhdGUoJzIwMjYtMDMtMDZUMTE6MzM6MzItMDY6MDAnKTsKCmNvbnN0IHRhYnMgPSBbCiAgeyBpZDogJ2Rhc2hib2FyZCcsIGxhYmVsOiAn8J+TiiBEYXNoYm9hcmQnIH0sCiAgeyBpZDogJ3Zpb2xhdGlvbicsIGxhYmVsOiAnKyBOZXcgVmlvbGF0aW9uJyB9LApdOwoKLyog4pSA4pSAIGhlbHBlcnMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSAICovCmZ1bmN0aW9uIGVsYXBzZWQoZnJvbSkgewogIGNvbnN0IGRpZmYgPSBNYXRoLmZsb29yKChEYXRlLm5vdygpIC0gZnJvbS5nZXRUaW1lKCkpIC8gMTAwMCk7CiAgY29uc3QgZCA9IE1hdGguZmxvb3IoZGlmZiAvIDg2NDAwKTsKICBjb25zdCBoID0gTWF0aC5mbG9vcigoZGlmZiAlIDg2NDAwKSAvIDM2MDApOwogIGNvbnN0IG0gPSBNYXRoLmZsb29yKChkaWZmICUgMzYwMCkgLyA2MCk7CiAgY29uc3QgcyA9IGRpZmYgJSA2MDsKICByZXR1cm4gYCR7ZH1kICR7U3RyaW5nKGgpLnBhZFN0YXJ0KDIsJzAnKX1oICR7U3RyaW5nKG0pLnBhZFN0YXJ0KDIsJzAnKX1tICR7U3RyaW5nKHMpLnBhZFN0YXJ0KDIsJzAnKX1zYDsKfQoKLyog4pSA4pSAIHN1Yi1jb21wb25lbnRzIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgCAqLwpmdW5jdGlvbiBEZXZUaWNrZXIoKSB7CiAgY29uc3QgW3RpY2ssIHNldFRpY2tdID0gdXNlU3RhdGUoKCkgPT4gZWxhcHNlZChQUk9KRUNUX1NUQVJUKSk7CiAgdXNlRWZmZWN0KCgpID0+IHsKICAgIGNvbnN0IGlkID0gc2V0SW50ZXJ2YWwoKCkgPT4gc2V0VGljayhlbGFwc2VkKFBST0pFQ1RfU1RBUlQpKSwgMTAwMCk7CiAgICByZXR1cm4gKCkgPT4gY2xlYXJJbnRlcnZhbChpZCk7CiAgfSwgW10pOwogIHJldHVybiAoCiAgICA8c3BhbiBzdHlsZT17c2YudGlja2VyfSB0aXRsZT0iVGltZSBzaW5jZSBmaXJzdCBjb21taXQiPgogICAgICA8c3BhbiBzdHlsZT17c2YudGlja2VyRG90fSAvPgogICAgICB7dGlja30KICAgIDwvc3Bhbj4KICApOwp9CgpmdW5jdGlvbiBHaXRlYUljb24oKSB7CiAgLy8gU2ltcGxlIEdpdGVhLXN0eWxlIGljb24gdXNpbmcgU1ZHIHBhdGgKICByZXR1cm4gKAogICAgPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iY3VycmVudENvbG9yIiBzdHlsZT17eyBkaXNwbGF5OidibG9jaycgfX0+CiAgICAgIDxwYXRoIGQ9Ik0xMiAyQzYuNDc3IDIgMiA2LjQ3NyAyIDEyYzAgNC40MTggMi44NjUgOC4xNjYgNi44MzkgOS40ODkuNS4wOTIuNjgyLS4yMTcuNjgyLS40ODIKICAgICAgICAgICAgICAgMC0uMjM3LS4wMDktLjg2OC0uMDEzLTEuNzAzLTIuNzgyLjYwNS0zLjM2OS0xLjM0LTMuMzY5LTEuMzQtLjQ1NC0xLjE1NC0xLjExLTEuNDYyCiAgICAgICAgICAgICAgIC0xLjExLTEuNDYyLS45MDgtLjYyLjA2OS0uNjA4LjA2OS0uNjA4IDEuMDAzLjA3IDEuNTMxIDEuMDMgMS41MzEgMS4wMy44OTIgMS41MjkKICAgICAgICAgICAgICAgMi4zNDEgMS4wODcgMi45MS44MzIuMDkyLS42NDcuMzUtMS4wODguNjM2LTEuMzM4LTIuMjItLjI1My00LjU1NS0xLjExLTQuNTU1LTQuOTQzCiAgICAgICAgICAgICAgIDAtMS4wOTEuMzktMS45ODQgMS4wMjktMi42ODMtLjEwMy0uMjUzLS40NDYtMS4yNy4wOTgtMi42NDcgMCAwIC44NC0uMjY5IDIuNzUgMS4wMjUKICAgICAgICAgICAgICAgQTkuNTY0IDkuNTY0IDAgMDExMiA2Ljg0NGE5LjU5IDkuNTkgMCAwMTIuNTA0LjMzN2MxLjkwOS0xLjI5NCAyLjc0Ny0xLjAyNSAyLjc0Ny0xLjAyNQogICAgICAgICAgICAgICAuNTQ2IDEuMzc3LjIwMiAyLjM5NC4xIDIuNjQ3LjY0LjY5OSAxLjAyOCAxLjU5MiAxLjAyOCAyLjY4MyAwIDMuODQyLTIuMzM5IDQuNjg3CiAgICAgICAgICAgICAgIC00LjU2NiA0LjkzNS4zNTkuMzA5LjY3OC45MTkuNjc4IDEuODUyIDAgMS4zMzYtLjAxMiAyLjQxNS0uMDEyIDIuNzQzIDAgLjI2Ny4xOC41NzgKICAgICAgICAgICAgICAgLjY4OC40OEMxOS4xMzggMjAuMTYzIDIyIDE2LjQxOCAyMiAxMmMwLTUuNTIzLTQuNDc3LTEwLTEwLTEweiIvPgogICAgPC9zdmc+CiAgKTsKfQoKZnVuY3Rpb24gQXBwRm9vdGVyKCkgewogIHJldHVybiAoCiAgICA8Zm9vdGVyIHN0eWxlPXtzZi5mb290ZXJ9PgogICAgICA8c3BhbiBzdHlsZT17c2YuY29weX0+wqkge25ldyBEYXRlKCkuZ2V0RnVsbFllYXIoKX0gSmFzb24gU3RlZHdlbGw8L3NwYW4+CiAgICAgIDxzcGFuIHN0eWxlPXtzZi5zZXB9PsK3PC9zcGFuPgogICAgICA8RGV2VGlja2VyIC8+CiAgICAgIDxzcGFuIHN0eWxlPXtzZi5zZXB9PsK3PC9zcGFuPgogICAgICA8YSBocmVmPXtSRVBPX1VSTH0gdGFyZ2V0PSJfYmxhbmsiIHJlbD0ibm9vcGVuZXIgbm9yZWZlcnJlciIgc3R5bGU9e3NmLnJlcG9MaW5rfSB0aXRsZT0iVmlldyBzb3VyY2Ugb24gR2l0ZWEiPgogICAgICAgIDxHaXRlYUljb24gLz4KICAgICAgICA8c3Bhbj5jcGFzPC9zcGFuPgogICAgICA8L2E+CiAgICA8L2Zvb3Rlcj4KICApOwp9CgovKiDilIDilIAgc3R5bGVzIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgCAqLwpjb25zdCBzID0gewogIGFwcDogICAgICB7IG1pbkhlaWdodDogJzEwMHZoJywgYmFja2dyb3VuZDogJyMwNTA2MDgnLCBmb250RmFtaWx5OiAiJ1NlZ29lIFVJJywgQXJpYWwsIHNhbnMtc2VyaWYiLCBjb2xvcjogJyNmOGY5ZmEnLCBkaXNwbGF5OiAnZmxleCcsIGZsZXhEaXJlY3Rpb246ICdjb2x1bW4nIH0sCiAgbmF2OiAgICAgIHsgYmFja2dyb3VuZDogJyMwMDAwMDAnLCBwYWRkaW5nOiAnMCA0MHB4JywgZGlzcGxheTogJ2ZsZXgnLCBhbGlnbkl0ZW1zOiAnY2VudGVyJywgZ2FwOiAwLCBib3JkZXJCb3R0b206ICcxcHggc29saWQgIzMzMycgfSwKICBsb2dvV3JhcDogeyBkaXNwbGF5OiAnZmxleCcsIGFsaWduSXRlbXM6ICdjZW50ZXInLCBtYXJnaW5SaWdodDogJzMycHgnLCBwYWRkaW5nOiAnMTRweCAwJyB9LAogIGxvZ29JbWc6ICB7IGhlaWdodDogJzI4cHgnLCBtYXJnaW5SaWdodDogJzEwcHgnIH0sCiAgbG9nb1RleHQ6IHsgY29sb3I6ICcjZjhmOWZhJywgZm9udFdlaWdodDogODAwLCBmb250U2l6ZTogJzE4cHgnLCBsZXR0ZXJTcGFjaW5nOiAnMC41cHgnIH0sCiAgdGFiOiAoYWN0aXZlKSA9PiAoewogICAgcGFkZGluZzogJzE4cHggMjJweCcsCiAgICBjb2xvcjogYWN0aXZlID8gJyNmOGY5ZmEnIDogJ3JnYmEoMjQ4LDI0OSwyNTAsMC42KScsCiAgICBib3JkZXJCb3R0b206IGFjdGl2ZSA/ICczcHggc29saWQgI2Q0YWYzNycgOiAnM3B4IHNvbGlkIHRyYW5zcGFyZW50JywKICAgIGN1cnNvcjogJ3BvaW50ZXInLCBmb250V2VpZ2h0OiBhY3RpdmUgPyA3MDAgOiA0MDAsIGZvbnRTaXplOiAnMTRweCcsCiAgICBiYWNrZ3JvdW5kOiAnbm9uZScsIGJvcmRlcjogJ25vbmUnLAogIH0pLAogIGRvY3NCdG46IHsKICAgIG1hcmdpbkxlZnQ6ICdhdXRvJywKICAgIGJhY2tncm91bmQ6ICdub25lJywKICAgIGJvcmRlcjogJzFweCBzb2xpZCAjMmEyYjNhJywKICAgIGNvbG9yOiAnIzljYTBiOCcsCiAgICBib3JkZXJSYWRpdXM6ICc2cHgnLAogICAgcGFkZGluZzogJzZweCAxNHB4JywKICAgIGZvbnRTaXplOiAnMTJweCcsCiAgICBjdXJzb3I6ICdwb2ludGVyJywKICAgIGZvbnRXZWlnaHQ6IDYwMCwKICAgIGxldHRlclNwYWNpbmc6ICcwLjNweCcsCiAgICBkaXNwbGF5OiAnZmxleCcsCiAgICBhbGlnbkl0ZW1zOiAnY2VudGVyJywKICAgIGdhcDogJzZweCcsCiAgfSwKICBtYWluOiB7IGZsZXg6IDEgfSwKICBjYXJkOiB7IG1heFdpZHRoOiAnMTEwMHB4JywgbWFyZ2luOiAnMzBweCBhdXRvJywgYmFja2dyb3VuZDogJyMxMTEyMTcnLCBib3JkZXJSYWRpdXM6ICcxMHB4JywgYm94U2hhZG93OiAnMCAycHggMTZweCByZ2JhKDAsMCwwLDAuNiknLCBib3JkZXI6ICcxcHggc29saWQgIzIyMicgfSwKfTsKCi8vIEZvb3Rlci1zcGVjaWZpYyBzdHlsZXMga2VwdCBzZXBhcmF0ZSBmb3IgY2xhcml0eQpjb25zdCBzZiA9IHsKICBmb290ZXI6IHsKICAgIGJhY2tncm91bmQ6ICcjMDAwJywKICAgIGJvcmRlclRvcDogJzFweCBzb2xpZCAjMWUxZjJlJywKICAgIHBhZGRpbmc6ICcxMnB4IDQwcHgnLAogICAgZGlzcGxheTogJ2ZsZXgnLAogICAgYWxpZ25JdGVtczogJ2NlbnRlcicsCiAgICBnYXA6ICcxMHB4JywKICAgIGZvbnRTaXplOiAnMTJweCcsCiAgICBjb2xvcjogJyM0YTRkNjYnLAogICAgZmxleFdyYXA6ICd3cmFwJywKICB9LAogIGNvcHk6ICAgICAgeyBjb2xvcjogJyM0YTRkNjYnLCB3aGl0ZVNwYWNlOiAnbm93cmFwJyB9LAogIHNlcDogICAgICAgeyBjb2xvcjogJyMyYTJiM2EnLCB1c2VyU2VsZWN0OiAnbm9uZScgfSwKICB0aWNrZXI6IHsKICAgIGRpc3BsYXk6ICdpbmxpbmUtZmxleCcsCiAgICBhbGlnbkl0ZW1zOiAnY2VudGVyJywKICAgIGdhcDogJzZweCcsCiAgICBmb250RmFtaWx5OiAiJ0NvdXJpZXIgTmV3JywgbW9ub3NwYWNlIiwKICAgIGZvbnRTaXplOiAnMTFweCcsCiAgICBjb2xvcjogJyM1YTdhNWEnLAogICAgbGV0dGVyU3BhY2luZzogJzAuNXB4JywKICB9LAogIHRpY2tlckRvdDogewogICAgd2lkdGg6ICc2cHgnLAogICAgaGVpZ2h0OiAnNnB4JywKICAgIGJvcmRlclJhZGl1czogJzUwJScsCiAgICBiYWNrZ3JvdW5kOiAnIzNhNmEzYScsCiAgICBib3hTaGFkb3c6ICcwIDAgNHB4ICMzYTZhM2EnLAogICAgZmxleFNocmluazogMCwKICB9LAogIHJlcG9MaW5rOiB7CiAgICBkaXNwbGF5OiAnaW5saW5lLWZsZXgnLAogICAgYWxpZ25JdGVtczogJ2NlbnRlcicsCiAgICBnYXA6ICc1cHgnLAogICAgY29sb3I6ICcjNGE0ZDY2JywKICAgIHRleHREZWNvcmF0aW9uOiAnbm9uZScsCiAgICB0cmFuc2l0aW9uOiAnY29sb3IgMC4xNXMnLAogIH0sCn07CgovKiDilIDilIAgbWFpbiBhcHAg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSAICovCmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIEFwcCgpIHsKICBjb25zdCBbdGFiLCAgICAgICAgc2V0VGFiXSAgICAgICAgPSB1c2VTdGF0ZSgnZGFzaGJvYXJkJyk7CiAgY29uc3QgW3Nob3dSZWFkbWUsIHNldFNob3dSZWFkbWVdID0gdXNlU3RhdGUoZmFsc2UpOwoKICByZXR1cm4gKAogICAgPFRvYXN0UHJvdmlkZXI+CiAgICAgIDxkaXYgc3R5bGU9e3MuYXBwfT4KICAgICAgICA8bmF2IHN0eWxlPXtzLm5hdn0+CiAgICAgICAgICA8ZGl2IHN0eWxlPXtzLmxvZ29XcmFwfT4KICAgICAgICAgICAgPGltZyBzcmM9Ii9zdGF0aWMvbXBtLWxvZ28ucG5nIiBhbHQ9Ik1QTSIgc3R5bGU9e3MubG9nb0ltZ30gLz4KICAgICAgICAgICAgPGRpdiBzdHlsZT17cy5sb2dvVGV4dH0+Q1BBUyBUcmFja2VyPC9kaXY+CiAgICAgICAgICA8L2Rpdj4KCiAgICAgICAgICB7dGFicy5tYXAodCA9PiAoCiAgICAgICAgICAgIDxidXR0b24ga2V5PXt0LmlkfSBzdHlsZT17cy50YWIodGFiID09PSB0LmlkKX0gb25DbGljaz17KCkgPT4gc2V0VGFiKHQuaWQpfT4KICAgICAgICAgICAgICB7dC5sYWJlbH0KICAgICAgICAgICAgPC9idXR0b24+CiAgICAgICAgICApKX0KCiAgICAgICAgICA8YnV0dG9uIHN0eWxlPXtzLmRvY3NCdG59IG9uQ2xpY2s9eygpID0+IHNldFNob3dSZWFkbWUodHJ1ZSl9IHRpdGxlPSJPcGVuIGFkbWluIGRvY3VtZW50YXRpb24iPgogICAgICAgICAgICA8c3Bhbj4/PC9zcGFuPiBEb2NzCiAgICAgICAgICA8L2J1dHRvbj4KICAgICAgICA8L25hdj4KCiAgICAgICAgPGRpdiBzdHlsZT17cy5tYWlufT4KICAgICAgICAgIDxkaXYgc3R5bGU9e3MuY2FyZH0+CiAgICAgICAgICAgIHt0YWIgPT09ICdkYXNoYm9hcmQnID8gPERhc2hib2FyZCAvPiA6IDxWaW9sYXRpb25Gb3JtIC8+fQogICAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CgogICAgICAgIDxBcHBGb290ZXIgLz4KCiAgICAgICAge3Nob3dSZWFkbWUgJiYgPFJlYWRtZU1vZGFsIG9uQ2xvc2U9eygpID0+IHNldFNob3dSZWFkbWUoZmFsc2UpfSAvPn0KICAgICAgPC9kaXY+CiAgICA8L1RvYXN0UHJvdmlkZXI+CiAgKTsKfQo= \ No newline at end of file -- 2.52.0 From 5f0ae959ede4dca006298447ddc3dd42514bacc1 Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 8 Mar 2026 00:25:11 -0600 Subject: [PATCH 10/40] Update client/src/App.jsx --- client/src/App.jsx | 77 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/client/src/App.jsx b/client/src/App.jsx index df8dadf..b3645c2 100755 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1 +1,76 @@ -aW1wb3J0IFJlYWN0LCB7IHVzZVN0YXRlLCB1c2VFZmZlY3QgfSBmcm9tICdyZWFjdCc7CmltcG9ydCBWaW9sYXRpb25Gb3JtIGZyb20gJy4vY29tcG9uZW50cy9WaW9sYXRpb25Gb3JtJzsKaW1wb3J0IERhc2hib2FyZCAgICAgZnJvbSAnLi9jb21wb25lbnRzL0Rhc2hib2FyZCc7CmltcG9ydCBSZWFkbWVNb2RhbCAgIGZyb20gJy4vY29tcG9uZW50cy9SZWFkbWVNb2RhbCc7CmltcG9ydCBUb2FzdFByb3ZpZGVyIGZyb20gJy4vY29tcG9uZW50cy9Ub2FzdFByb3ZpZGVyJzsKCmNvbnN0IFJFUE9fVVJMICAgICAgPSAnaHR0cHM6Ly9naXQuYWx3aXNwLmNvbS9qYXNvbi9jcGFzJzsKLy8gRmlyc3QgY29tbWl0IHRpbWVzdGFtcCDigJQgdXNlZCB0byBkcml2ZSB0aGUgZGV2IHRpY2tlcgpjb25zdCBQUk9KRUNUX1NUQVJUID0gbmV3IERhdGUoJzIwMjYtMDMtMDZUMTE6MzM6MzItMDY6MDAnKTsKCmNvbnN0IHRhYnMgPSBbCiAgeyBpZDogJ2Rhc2hib2FyZCcsIGxhYmVsOiAn8J+TiiBEYXNoYm9hcmQnIH0sCiAgeyBpZDogJ3Zpb2xhdGlvbicsIGxhYmVsOiAnKyBOZXcgVmlvbGF0aW9uJyB9LApdOwoKLyog4pSA4pSAIGhlbHBlcnMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSAICovCmZ1bmN0aW9uIGVsYXBzZWQoZnJvbSkgewogIGNvbnN0IGRpZmYgPSBNYXRoLmZsb29yKChEYXRlLm5vdygpIC0gZnJvbS5nZXRUaW1lKCkpIC8gMTAwMCk7CiAgY29uc3QgZCA9IE1hdGguZmxvb3IoZGlmZiAvIDg2NDAwKTsKICBjb25zdCBoID0gTWF0aC5mbG9vcigoZGlmZiAlIDg2NDAwKSAvIDM2MDApOwogIGNvbnN0IG0gPSBNYXRoLmZsb29yKChkaWZmICUgMzYwMCkgLyA2MCk7CiAgY29uc3QgcyA9IGRpZmYgJSA2MDsKICByZXR1cm4gYCR7ZH1kICR7U3RyaW5nKGgpLnBhZFN0YXJ0KDIsJzAnKX1oICR7U3RyaW5nKG0pLnBhZFN0YXJ0KDIsJzAnKX1tICR7U3RyaW5nKHMpLnBhZFN0YXJ0KDIsJzAnKX1zYDsKfQoKLyog4pSA4pSAIHN1Yi1jb21wb25lbnRzIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgCAqLwpmdW5jdGlvbiBEZXZUaWNrZXIoKSB7CiAgY29uc3QgW3RpY2ssIHNldFRpY2tdID0gdXNlU3RhdGUoKCkgPT4gZWxhcHNlZChQUk9KRUNUX1NUQVJUKSk7CiAgdXNlRWZmZWN0KCgpID0+IHsKICAgIGNvbnN0IGlkID0gc2V0SW50ZXJ2YWwoKCkgPT4gc2V0VGljayhlbGFwc2VkKFBST0pFQ1RfU1RBUlQpKSwgMTAwMCk7CiAgICByZXR1cm4gKCkgPT4gY2xlYXJJbnRlcnZhbChpZCk7CiAgfSwgW10pOwogIHJldHVybiAoCiAgICA8c3BhbiBzdHlsZT17c2YudGlja2VyfSB0aXRsZT0iVGltZSBzaW5jZSBmaXJzdCBjb21taXQiPgogICAgICA8c3BhbiBzdHlsZT17c2YudGlja2VyRG90fSAvPgogICAgICB7dGlja30KICAgIDwvc3Bhbj4KICApOwp9CgpmdW5jdGlvbiBHaXRlYUljb24oKSB7CiAgLy8gU2ltcGxlIEdpdGVhLXN0eWxlIGljb24gdXNpbmcgU1ZHIHBhdGgKICByZXR1cm4gKAogICAgPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iY3VycmVudENvbG9yIiBzdHlsZT17eyBkaXNwbGF5OidibG9jaycgfX0+CiAgICAgIDxwYXRoIGQ9Ik0xMiAyQzYuNDc3IDIgMiA2LjQ3NyAyIDEyYzAgNC40MTggMi44NjUgOC4xNjYgNi44MzkgOS40ODkuNS4wOTIuNjgyLS4yMTcuNjgyLS40ODIKICAgICAgICAgICAgICAgMC0uMjM3LS4wMDktLjg2OC0uMDEzLTEuNzAzLTIuNzgyLjYwNS0zLjM2OS0xLjM0LTMuMzY5LTEuMzQtLjQ1NC0xLjE1NC0xLjExLTEuNDYyCiAgICAgICAgICAgICAgIC0xLjExLTEuNDYyLS45MDgtLjYyLjA2OS0uNjA4LjA2OS0uNjA4IDEuMDAzLjA3IDEuNTMxIDEuMDMgMS41MzEgMS4wMy44OTIgMS41MjkKICAgICAgICAgICAgICAgMi4zNDEgMS4wODcgMi45MS44MzIuMDkyLS42NDcuMzUtMS4wODguNjM2LTEuMzM4LTIuMjItLjI1My00LjU1NS0xLjExLTQuNTU1LTQuOTQzCiAgICAgICAgICAgICAgIDAtMS4wOTEuMzktMS45ODQgMS4wMjktMi42ODMtLjEwMy0uMjUzLS40NDYtMS4yNy4wOTgtMi42NDcgMCAwIC44NC0uMjY5IDIuNzUgMS4wMjUKICAgICAgICAgICAgICAgQTkuNTY0IDkuNTY0IDAgMDExMiA2Ljg0NGE5LjU5IDkuNTkgMCAwMTIuNTA0LjMzN2MxLjkwOS0xLjI5NCAyLjc0Ny0xLjAyNSAyLjc0Ny0xLjAyNQogICAgICAgICAgICAgICAuNTQ2IDEuMzc3LjIwMiAyLjM5NC4xIDIuNjQ3LjY0LjY5OSAxLjAyOCAxLjU5MiAxLjAyOCAyLjY4MyAwIDMuODQyLTIuMzM5IDQuNjg3CiAgICAgICAgICAgICAgIC00LjU2NiA0LjkzNS4zNTkuMzA5LjY3OC45MTkuNjc4IDEuODUyIDAgMS4zMzYtLjAxMiAyLjQxNS0uMDEyIDIuNzQzIDAgLjI2Ny4xOC41NzgKICAgICAgICAgICAgICAgLjY4OC40OEMxOS4xMzggMjAuMTYzIDIyIDE2LjQxOCAyMiAxMmMwLTUuNTIzLTQuNDc3LTEwLTEwLTEweiIvPgogICAgPC9zdmc+CiAgKTsKfQoKZnVuY3Rpb24gQXBwRm9vdGVyKCkgewogIHJldHVybiAoCiAgICA8Zm9vdGVyIHN0eWxlPXtzZi5mb290ZXJ9PgogICAgICA8c3BhbiBzdHlsZT17c2YuY29weX0+wqkge25ldyBEYXRlKCkuZ2V0RnVsbFllYXIoKX0gSmFzb24gU3RlZHdlbGw8L3NwYW4+CiAgICAgIDxzcGFuIHN0eWxlPXtzZi5zZXB9PsK3PC9zcGFuPgogICAgICA8RGV2VGlja2VyIC8+CiAgICAgIDxzcGFuIHN0eWxlPXtzZi5zZXB9PsK3PC9zcGFuPgogICAgICA8YSBocmVmPXtSRVBPX1VSTH0gdGFyZ2V0PSJfYmxhbmsiIHJlbD0ibm9vcGVuZXIgbm9yZWZlcnJlciIgc3R5bGU9e3NmLnJlcG9MaW5rfSB0aXRsZT0iVmlldyBzb3VyY2Ugb24gR2l0ZWEiPgogICAgICAgIDxHaXRlYUljb24gLz4KICAgICAgICA8c3Bhbj5jcGFzPC9zcGFuPgogICAgICA8L2E+CiAgICA8L2Zvb3Rlcj4KICApOwp9CgovKiDilIDilIAgc3R5bGVzIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgCAqLwpjb25zdCBzID0gewogIGFwcDogICAgICB7IG1pbkhlaWdodDogJzEwMHZoJywgYmFja2dyb3VuZDogJyMwNTA2MDgnLCBmb250RmFtaWx5OiAiJ1NlZ29lIFVJJywgQXJpYWwsIHNhbnMtc2VyaWYiLCBjb2xvcjogJyNmOGY5ZmEnLCBkaXNwbGF5OiAnZmxleCcsIGZsZXhEaXJlY3Rpb246ICdjb2x1bW4nIH0sCiAgbmF2OiAgICAgIHsgYmFja2dyb3VuZDogJyMwMDAwMDAnLCBwYWRkaW5nOiAnMCA0MHB4JywgZGlzcGxheTogJ2ZsZXgnLCBhbGlnbkl0ZW1zOiAnY2VudGVyJywgZ2FwOiAwLCBib3JkZXJCb3R0b206ICcxcHggc29saWQgIzMzMycgfSwKICBsb2dvV3JhcDogeyBkaXNwbGF5OiAnZmxleCcsIGFsaWduSXRlbXM6ICdjZW50ZXInLCBtYXJnaW5SaWdodDogJzMycHgnLCBwYWRkaW5nOiAnMTRweCAwJyB9LAogIGxvZ29JbWc6ICB7IGhlaWdodDogJzI4cHgnLCBtYXJnaW5SaWdodDogJzEwcHgnIH0sCiAgbG9nb1RleHQ6IHsgY29sb3I6ICcjZjhmOWZhJywgZm9udFdlaWdodDogODAwLCBmb250U2l6ZTogJzE4cHgnLCBsZXR0ZXJTcGFjaW5nOiAnMC41cHgnIH0sCiAgdGFiOiAoYWN0aXZlKSA9PiAoewogICAgcGFkZGluZzogJzE4cHggMjJweCcsCiAgICBjb2xvcjogYWN0aXZlID8gJyNmOGY5ZmEnIDogJ3JnYmEoMjQ4LDI0OSwyNTAsMC42KScsCiAgICBib3JkZXJCb3R0b206IGFjdGl2ZSA/ICczcHggc29saWQgI2Q0YWYzNycgOiAnM3B4IHNvbGlkIHRyYW5zcGFyZW50JywKICAgIGN1cnNvcjogJ3BvaW50ZXInLCBmb250V2VpZ2h0OiBhY3RpdmUgPyA3MDAgOiA0MDAsIGZvbnRTaXplOiAnMTRweCcsCiAgICBiYWNrZ3JvdW5kOiAnbm9uZScsIGJvcmRlcjogJ25vbmUnLAogIH0pLAogIGRvY3NCdG46IHsKICAgIG1hcmdpbkxlZnQ6ICdhdXRvJywKICAgIGJhY2tncm91bmQ6ICdub25lJywKICAgIGJvcmRlcjogJzFweCBzb2xpZCAjMmEyYjNhJywKICAgIGNvbG9yOiAnIzljYTBiOCcsCiAgICBib3JkZXJSYWRpdXM6ICc2cHgnLAogICAgcGFkZGluZzogJzZweCAxNHB4JywKICAgIGZvbnRTaXplOiAnMTJweCcsCiAgICBjdXJzb3I6ICdwb2ludGVyJywKICAgIGZvbnRXZWlnaHQ6IDYwMCwKICAgIGxldHRlclNwYWNpbmc6ICcwLjNweCcsCiAgICBkaXNwbGF5OiAnZmxleCcsCiAgICBhbGlnbkl0ZW1zOiAnY2VudGVyJywKICAgIGdhcDogJzZweCcsCiAgfSwKICBtYWluOiB7IGZsZXg6IDEgfSwKICBjYXJkOiB7IG1heFdpZHRoOiAnMTEwMHB4JywgbWFyZ2luOiAnMzBweCBhdXRvJywgYmFja2dyb3VuZDogJyMxMTEyMTcnLCBib3JkZXJSYWRpdXM6ICcxMHB4JywgYm94U2hhZG93OiAnMCAycHggMTZweCByZ2JhKDAsMCwwLDAuNiknLCBib3JkZXI6ICcxcHggc29saWQgIzIyMicgfSwKfTsKCi8vIEZvb3Rlci1zcGVjaWZpYyBzdHlsZXMga2VwdCBzZXBhcmF0ZSBmb3IgY2xhcml0eQpjb25zdCBzZiA9IHsKICBmb290ZXI6IHsKICAgIGJhY2tncm91bmQ6ICcjMDAwJywKICAgIGJvcmRlclRvcDogJzFweCBzb2xpZCAjMWUxZjJlJywKICAgIHBhZGRpbmc6ICcxMnB4IDQwcHgnLAogICAgZGlzcGxheTogJ2ZsZXgnLAogICAgYWxpZ25JdGVtczogJ2NlbnRlcicsCiAgICBnYXA6ICcxMHB4JywKICAgIGZvbnRTaXplOiAnMTJweCcsCiAgICBjb2xvcjogJyM0YTRkNjYnLAogICAgZmxleFdyYXA6ICd3cmFwJywKICB9LAogIGNvcHk6ICAgICAgeyBjb2xvcjogJyM0YTRkNjYnLCB3aGl0ZVNwYWNlOiAnbm93cmFwJyB9LAogIHNlcDogICAgICAgeyBjb2xvcjogJyMyYTJiM2EnLCB1c2VyU2VsZWN0OiAnbm9uZScgfSwKICB0aWNrZXI6IHsKICAgIGRpc3BsYXk6ICdpbmxpbmUtZmxleCcsCiAgICBhbGlnbkl0ZW1zOiAnY2VudGVyJywKICAgIGdhcDogJzZweCcsCiAgICBmb250RmFtaWx5OiAiJ0NvdXJpZXIgTmV3JywgbW9ub3NwYWNlIiwKICAgIGZvbnRTaXplOiAnMTFweCcsCiAgICBjb2xvcjogJyM1YTdhNWEnLAogICAgbGV0dGVyU3BhY2luZzogJzAuNXB4JywKICB9LAogIHRpY2tlckRvdDogewogICAgd2lkdGg6ICc2cHgnLAogICAgaGVpZ2h0OiAnNnB4JywKICAgIGJvcmRlclJhZGl1czogJzUwJScsCiAgICBiYWNrZ3JvdW5kOiAnIzNhNmEzYScsCiAgICBib3hTaGFkb3c6ICcwIDAgNHB4ICMzYTZhM2EnLAogICAgZmxleFNocmluazogMCwKICB9LAogIHJlcG9MaW5rOiB7CiAgICBkaXNwbGF5OiAnaW5saW5lLWZsZXgnLAogICAgYWxpZ25JdGVtczogJ2NlbnRlcicsCiAgICBnYXA6ICc1cHgnLAogICAgY29sb3I6ICcjNGE0ZDY2JywKICAgIHRleHREZWNvcmF0aW9uOiAnbm9uZScsCiAgICB0cmFuc2l0aW9uOiAnY29sb3IgMC4xNXMnLAogIH0sCn07CgovKiDilIDilIAgbWFpbiBhcHAg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSAICovCmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIEFwcCgpIHsKICBjb25zdCBbdGFiLCAgICAgICAgc2V0VGFiXSAgICAgICAgPSB1c2VTdGF0ZSgnZGFzaGJvYXJkJyk7CiAgY29uc3QgW3Nob3dSZWFkbWUsIHNldFNob3dSZWFkbWVdID0gdXNlU3RhdGUoZmFsc2UpOwoKICByZXR1cm4gKAogICAgPFRvYXN0UHJvdmlkZXI+CiAgICAgIDxkaXYgc3R5bGU9e3MuYXBwfT4KICAgICAgICA8bmF2IHN0eWxlPXtzLm5hdn0+CiAgICAgICAgICA8ZGl2IHN0eWxlPXtzLmxvZ29XcmFwfT4KICAgICAgICAgICAgPGltZyBzcmM9Ii9zdGF0aWMvbXBtLWxvZ28ucG5nIiBhbHQ9Ik1QTSIgc3R5bGU9e3MubG9nb0ltZ30gLz4KICAgICAgICAgICAgPGRpdiBzdHlsZT17cy5sb2dvVGV4dH0+Q1BBUyBUcmFja2VyPC9kaXY+CiAgICAgICAgICA8L2Rpdj4KCiAgICAgICAgICB7dGFicy5tYXAodCA9PiAoCiAgICAgICAgICAgIDxidXR0b24ga2V5PXt0LmlkfSBzdHlsZT17cy50YWIodGFiID09PSB0LmlkKX0gb25DbGljaz17KCkgPT4gc2V0VGFiKHQuaWQpfT4KICAgICAgICAgICAgICB7dC5sYWJlbH0KICAgICAgICAgICAgPC9idXR0b24+CiAgICAgICAgICApKX0KCiAgICAgICAgICA8YnV0dG9uIHN0eWxlPXtzLmRvY3NCdG59IG9uQ2xpY2s9eygpID0+IHNldFNob3dSZWFkbWUodHJ1ZSl9IHRpdGxlPSJPcGVuIGFkbWluIGRvY3VtZW50YXRpb24iPgogICAgICAgICAgICA8c3Bhbj4/PC9zcGFuPiBEb2NzCiAgICAgICAgICA8L2J1dHRvbj4KICAgICAgICA8L25hdj4KCiAgICAgICAgPGRpdiBzdHlsZT17cy5tYWlufT4KICAgICAgICAgIDxkaXYgc3R5bGU9e3MuY2FyZH0+CiAgICAgICAgICAgIHt0YWIgPT09ICdkYXNoYm9hcmQnID8gPERhc2hib2FyZCAvPiA6IDxWaW9sYXRpb25Gb3JtIC8+fQogICAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CgogICAgICAgIDxBcHBGb290ZXIgLz4KCiAgICAgICAge3Nob3dSZWFkbWUgJiYgPFJlYWRtZU1vZGFsIG9uQ2xvc2U9eygpID0+IHNldFNob3dSZWFkbWUoZmFsc2UpfSAvPn0KICAgICAgPC9kaXY+CiAgICA8L1RvYXN0UHJvdmlkZXI+CiAgKTsKfQo= \ No newline at end of file +import React, { useState } from 'react'; +import ViolationForm from './components/ViolationForm'; +import Dashboard from './components/Dashboard'; +import ReadmeModal from './components/ReadmeModal'; +import ToastProvider from './components/ToastProvider'; + +const tabs = [ + { id: 'dashboard', label: '📊 Dashboard' }, + { id: 'violation', label: '+ New Violation' }, +]; + +const s = { + app: { minHeight: '100vh', background: '#050608', fontFamily: "'Segoe UI', Arial, sans-serif", color: '#f8f9fa' }, + nav: { background: '#000000', padding: '0 40px', display: 'flex', alignItems: 'center', gap: 0, borderBottom: '1px solid #333' }, + logoWrap: { display: 'flex', alignItems: 'center', marginRight: '32px', padding: '14px 0' }, + logoImg: { height: '28px', marginRight: '10px' }, + logoText: { color: '#f8f9fa', fontWeight: 800, fontSize: '18px', letterSpacing: '0.5px' }, + tab: (active) => ({ + padding: '18px 22px', + color: active ? '#f8f9fa' : 'rgba(248,249,250,0.6)', + borderBottom: active ? '3px solid #d4af37' : '3px solid transparent', + cursor: 'pointer', fontWeight: active ? 700 : 400, fontSize: '14px', + background: 'none', border: 'none', + }), + // Docs button sits flush-right in the nav + docsBtn: { + marginLeft: 'auto', + background: 'none', + border: '1px solid #2a2b3a', + color: '#9ca0b8', + borderRadius: '6px', + padding: '6px 14px', + fontSize: '12px', + cursor: 'pointer', + fontWeight: 600, + letterSpacing: '0.3px', + display: 'flex', + alignItems: 'center', + gap: '6px', + }, + card: { maxWidth: '1100px', margin: '30px auto', background: '#111217', borderRadius: '10px', boxShadow: '0 2px 16px rgba(0,0,0,0.6)', border: '1px solid #222' }, +}; + +export default function App() { + const [tab, setTab] = useState('dashboard'); + const [showReadme, setShowReadme] = useState(false); + + return ( + +
+ + +
+ {tab === 'dashboard' ? : } +
+ + {showReadme && setShowReadme(false)} />} +
+
+ ); +} -- 2.52.0 From 7326ffec6eaf61a1d21b9cb93ae3cfe66e8cc79a Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 8 Mar 2026 00:32:35 -0600 Subject: [PATCH 11/40] docs: update README with Phase 8 features, expanded roadmap with effort ratings --- README.md | 120 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 85 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 9e3f23f..891be75 100755 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ 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](https://git.alwisp.com/jason/cpas) + --- ## The only requirement on your machine: Docker Desktop @@ -113,6 +115,14 @@ Access the app at `http://10.2.0.14:3001` (or whatever static IP you assigned). --- +## 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. + +--- + ## Features ### Company Dashboard @@ -120,8 +130,9 @@ Access the app at `http://10.2.0.14:3001` (or whatever static IP you assigned). - 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 +- **Department filter**: pre-loaded dropdown of all departments for quick scoped views - Click any employee name to open their full profile modal -- **🔍 Audit Log** button — filterable, paginated view of all system write actions +- **📋 Audit Log** button — filterable, paginated view of all system write actions ### Violation Form - Select existing employee or enter new employee by name @@ -170,6 +181,11 @@ Access the app at `http://10.2.0.14:3001` (or whatever static IP you assigned). - 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 in `Xd HHh MMm SSs` format with a pulsing green dot +- **Gitea repo link** with icon — links directly to `git.alwisp.com/jason/cpas` + ### CPAS Tier System | Points | Tier | Label | @@ -209,7 +225,7 @@ Scores are computed over a **rolling 90-day window** (negated violations exclude | GET | `/api/dashboard` | All employees with active points + violation counts | | POST | `/api/violations` | Log a new violation (accepts `acknowledged_by`, `acknowledged_date`) | | 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/negated` | 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 | @@ -223,42 +239,43 @@ Scores are computed over a **rolling 90-day window** (negated violations exclude ``` cpas/ -├── Dockerfile # Multi-stage: builds React + runs Express w/ Chromium +├── Dockerfile # Multi-stage: builds React + runs Express w/ Chromium ├── .dockerignore -├── package.json # Backend (Express) deps -├── server.js # API + static file server +├── package.json # Backend (Express) deps +├── server.js # API + static file server ├── db/ -│ ├── schema.sql # Tables + 90-day active score view +│ ├── schema.sql # Tables + 90-day active score view │ └── 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 + ├── App.jsx # Root app + AppFooter (copyright, dev ticker, Gitea link) ├── data/ - │ └── violations.js # All CPAS violation definitions + groups + │ └── violations.js # All CPAS violation definitions + groups ├── hooks/ - │ └── useEmployeeIntelligence.js # Score + history hook + │ └── useEmployeeIntelligence.js # Score + history hook └── components/ - ├── CpasBadge.jsx # Tier badge + color logic - ├── TierWarning.jsx # Pre-submit tier crossing alert - ├── Dashboard.jsx # Company-wide leaderboard + audit log trigger - ├── ViolationForm.jsx # Violation entry form + ack signature fields - ├── 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 + ├── CpasBadge.jsx # Tier badge + color logic + ├── TierWarning.jsx # Pre-submit tier crossing alert + ├── Dashboard.jsx # Company-wide leaderboard + audit log trigger + ├── ViolationForm.jsx # Violation entry form + ack signature fields + ├── 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 ``` --- @@ -319,32 +336,65 @@ Point values, violation type, and incident date are **immutable** after submissi | 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 | --- ### 📋 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 | +| CSV export of dashboard | 🟢 | Client-side Blob download of the current filtered employee view; no backend changes needed | + #### Reporting & Analytics -- **Violation trends chart** — line/bar chart of violations per day/week/month, filterable by department or supervisor; useful for identifying systemic patterns vs. individual incidents -- **Department heat map** — grid view showing violation density and average CPAS score by department; helps supervisors identify team-level risk -- **CSV / Excel export** — bulk export of violations or dashboard data for external reporting or payroll integration + +| 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 | +| CSV / Excel bulk export | 🟡 | Full export of violations or dashboard data for external reporting or payroll integration | #### Employee Management -- **Supervisor view** — scoped dashboard showing only the employees under a given supervisor, useful for multi-supervisor environments + +| 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 -- **Draft / pending violations** — save a violation as draft before finalizing, useful when incidents need review before being officially logged -- **Bulk violation import** — CSV import for migrating historical records from paper logs or a prior system + +| Feature | Effort | Description | +|---------|--------|-------------| +| Draft / pending violations | 🟡 | Save a violation as draft before finalizing; useful when incidents need review before being officially logged | +| Bulk violation import | 🔴 | CSV import for migrating historical records from paper logs or a prior system | +| Violation templates | 🟢 | Pre-fill the form with a saved violation type + common details for frequently logged incidents | #### Notifications & Escalation -- **Tier escalation alerts** — email or in-app notification when an employee crosses into Tier 2+ so the relevant supervisor is automatically informed -- **Scheduled summary digest** — weekly email to supervisors listing their employees' current standings and any approaching tier thresholds -- **At-risk threshold configuration** — make the "at-risk" warning threshold (currently hardcoded at 2 pts) configurable per deployment + +| Feature | Effort | Description | +|---------|--------|-------------| +| Scheduled expiration digest | 🟡 | Weekly or daily email listing violations rolling off in the next 7 days; `nodemailer` + cron on the Node server | +| 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 -- **Multi-user auth** — simple login with role-based access (admin, supervisor, read-only); currently the app has no auth and is assumed to run on a trusted internal network -- **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 + +| 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 | +| 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 | --- -- 2.52.0 From 981fa3bea44cb8c79cb08976ad2de320788c53bb Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 8 Mar 2026 00:35:35 -0600 Subject: [PATCH 12/40] feat: add footer with copyright, live dev ticker, and Gitea repo link --- client/src/App.jsx | 101 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 6 deletions(-) diff --git a/client/src/App.jsx b/client/src/App.jsx index b3645c2..37cb06d 100755 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,17 +1,78 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import ViolationForm from './components/ViolationForm'; import Dashboard from './components/Dashboard'; import ReadmeModal from './components/ReadmeModal'; import ToastProvider from './components/ToastProvider'; +const REPO_URL = 'https://git.alwisp.com/jason/cpas'; +const PROJECT_START = new Date('2026-03-06T11:33:32-06:00'); + +function elapsed(from) { + const totalSec = Math.floor((Date.now() - from.getTime()) / 1000); + const d = Math.floor(totalSec / 86400); + const h = Math.floor((totalSec % 86400) / 3600); + const m = Math.floor((totalSec % 3600) / 60); + const s = totalSec % 60; + return `${d}d ${String(h).padStart(2,'0')}h ${String(m).padStart(2,'0')}m ${String(s).padStart(2,'0')}s`; +} + +function DevTicker() { + const [tick, setTick] = useState(() => elapsed(PROJECT_START)); + useEffect(() => { + const id = setInterval(() => setTick(elapsed(PROJECT_START)), 1000); + return () => clearInterval(id); + }, []); + return ( + + + {tick} + + ); +} + +function GiteaIcon() { + return ( + + + + ); +} + +function AppFooter() { + const year = new Date().getFullYear(); + return ( + <> + +
+ © {year} Jason Stedwell + · + + · + + cpas + +
+ + ); +} + const tabs = [ { id: 'dashboard', label: '📊 Dashboard' }, { id: 'violation', label: '+ New Violation' }, ]; const s = { - app: { minHeight: '100vh', background: '#050608', fontFamily: "'Segoe UI', Arial, sans-serif", color: '#f8f9fa' }, - nav: { background: '#000000', padding: '0 40px', display: 'flex', alignItems: 'center', gap: 0, borderBottom: '1px solid #333' }, + app: { minHeight: '100vh', background: '#050608', fontFamily: "'Segoe UI', Arial, sans-serif", color: '#f8f9fa', display: 'flex', flexDirection: 'column' }, + nav: { background: '#000000', padding: '0 40px', display: 'flex', alignItems: 'center', gap: 0, borderBottom: '1px solid #333' }, logoWrap: { display: 'flex', alignItems: 'center', marginRight: '32px', padding: '14px 0' }, logoImg: { height: '28px', marginRight: '10px' }, logoText: { color: '#f8f9fa', fontWeight: 800, fontSize: '18px', letterSpacing: '0.5px' }, @@ -22,7 +83,6 @@ const s = { cursor: 'pointer', fontWeight: active ? 700 : 400, fontSize: '14px', background: 'none', border: 'none', }), - // Docs button sits flush-right in the nav docsBtn: { marginLeft: 'auto', background: 'none', @@ -38,9 +98,34 @@ const s = { alignItems: 'center', gap: '6px', }, + main: { flex: 1 }, card: { maxWidth: '1100px', margin: '30px auto', background: '#111217', borderRadius: '10px', boxShadow: '0 2px 16px rgba(0,0,0,0.6)', border: '1px solid #222' }, }; +const sf = { + footer: { + borderTop: '1px solid #1a1b22', + padding: '12px 40px', + display: 'flex', + alignItems: 'center', + gap: '12px', + fontSize: '11px', + color: 'rgba(248,249,250,0.35)', + background: '#000', + flexShrink: 0, + }, + copy: { color: 'rgba(248,249,250,0.35)' }, + sep: { color: 'rgba(248,249,250,0.15)' }, + link: { + color: 'rgba(248,249,250,0.35)', + textDecoration: 'none', + display: 'inline-flex', + alignItems: 'center', + gap: '4px', + transition: 'color 0.15s', + }, +}; + export default function App() { const [tab, setTab] = useState('dashboard'); const [showReadme, setShowReadme] = useState(false); @@ -65,10 +150,14 @@ export default function App() { -
- {tab === 'dashboard' ? : } +
+
+ {tab === 'dashboard' ? : } +
+ + {showReadme && setShowReadme(false)} />}
-- 2.52.0 From 995e6070036ec773ed51de99e427dd5eb1fbd051 Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 8 Mar 2026 00:38:38 -0600 Subject: [PATCH 13/40] fix: remove default browser body margin causing white border --- client/index.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/index.html b/client/index.html index 2a311d7..a72bce6 100755 --- a/client/index.html +++ b/client/index.html @@ -4,6 +4,11 @@ CPAS Violation Tracker +
-- 2.52.0 From 87cf48e77e29bbd773dce5801164418132c9ed0c Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 8 Mar 2026 00:42:48 -0600 Subject: [PATCH 14/40] Update README.md --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 891be75..db9a258 100755 --- a/README.md +++ b/README.md @@ -353,7 +353,6 @@ Effort ratings: 🟢 Low · 🟡 Medium · 🔴 High | 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 | -| CSV export of dashboard | 🟢 | Client-side Blob download of the current filtered employee view; no backend changes needed | #### Reporting & Analytics @@ -362,7 +361,6 @@ Effort ratings: 🟢 Low · 🟡 Medium · 🔴 High | 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 | -| CSV / Excel bulk export | 🟡 | Full export of violations or dashboard data for external reporting or payroll integration | #### Employee Management @@ -376,14 +374,12 @@ Effort ratings: 🟢 Low · 🟡 Medium · 🔴 High | Feature | Effort | Description | |---------|--------|-------------| | Draft / pending violations | 🟡 | Save a violation as draft before finalizing; useful when incidents need review before being officially logged | -| Bulk violation import | 🔴 | CSV import for migrating historical records from paper logs or a prior system | | Violation templates | 🟢 | Pre-fill the form with a saved violation type + common details for frequently logged incidents | #### Notifications & Escalation | Feature | Effort | Description | |---------|--------|-------------| -| Scheduled expiration digest | 🟡 | Weekly or daily email listing violations rolling off in the next 7 days; `nodemailer` + cron on the Node server | | 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` | -- 2.52.0 From b02026f8a92f2e1e6ecd18f82e36cdaf920b4620 Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 8 Mar 2026 00:45:49 -0600 Subject: [PATCH 15/40] feat: inject git SHA + build timestamp into version.json during docker build --- Dockerfile | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 18091ea..5aa27ad 100755 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,15 @@ RUN cd client && npm install COPY client/ ./client/ RUN cd client && npm run build +# ── Version metadata ────────────────────────────────────────────────────────── +# Pass these at build time: +# docker build --build-arg GIT_SHA=$(git rev-parse HEAD) \ +# --build-arg BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) . +ARG GIT_SHA=dev +ARG BUILD_TIME=unknown +RUN echo "{\"sha\":\"${GIT_SHA}\",\"shortSha\":\"${GIT_SHA:0:7}\",\"buildTime\":\"${BUILD_TIME}\"}" \ + > /build/client/dist/version.json + FROM node:20-alpine AS production RUN apk add --no-cache chromium nss freetype harfbuzz ca-certificates ttf-freefont ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true @@ -25,5 +34,6 @@ COPY demo/ ./demo/ COPY client/public/static ./client/dist/static RUN mkdir -p /data EXPOSE 3001 -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 CMD wget -qO- http://localhost:3001/api/health || exit 1 +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -qO- http://localhost:3001/api/health || exit 1 CMD ["node", "server.js"] -- 2.52.0 From 20be30318f30c16b98a595f7f3202e71f6e6a974 Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 8 Mar 2026 00:45:53 -0600 Subject: [PATCH 16/40] feat: add local dev fallback version.json stub --- client/public/version.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 client/public/version.json diff --git a/client/public/version.json b/client/public/version.json new file mode 100644 index 0000000..d23d125 --- /dev/null +++ b/client/public/version.json @@ -0,0 +1,5 @@ +{ + "sha": "dev", + "shortSha": "dev", + "buildTime": null +} -- 2.52.0 From 51bf176f96c4d298c201544ded31fc2df17a1512 Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 8 Mar 2026 00:54:58 -0600 Subject: [PATCH 17/40] feat: load version.json at startup, expose via /api/health --- server.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/server.js b/server.js index d1dc66b..e4a220e 100755 --- a/server.js +++ b/server.js @@ -29,8 +29,19 @@ function audit(action, entityType, entityId, performedBy, details) { } } +// ── Version info (written by Dockerfile at build time) ─────────────────────── +// Falls back to { sha: 'dev' } when running outside a Docker build (local dev). +let BUILD_VERSION = { sha: 'dev', shortSha: 'dev', buildTime: null }; +try { + BUILD_VERSION = require('./client/dist/version.json'); +} catch (_) { /* pre-build or local dev — stub values are fine */ } + // Health -app.get('/api/health', (req, res) => res.json({ status: 'ok', timestamp: new Date().toISOString() })); +app.get('/api/health', (req, res) => res.json({ + status: 'ok', + timestamp: new Date().toISOString(), + version: BUILD_VERSION, +})); // ── Employees ──────────────────────────────────────────────────────────────── app.get('/api/employees', (req, res) => { -- 2.52.0 From f4ed8c49ce79d564e3f5f46469ccb3d977f1baa7 Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 8 Mar 2026 00:55:44 -0600 Subject: [PATCH 18/40] feat: fetch version.json on mount, show short SHA + commit link in footer --- client/src/App.jsx | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/client/src/App.jsx b/client/src/App.jsx index 37cb06d..0055663 100755 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -42,8 +42,13 @@ function GiteaIcon() { ); } -function AppFooter() { +function AppFooter({ version }) { const year = new Date().getFullYear(); + const sha = version?.shortSha || null; + const built = version?.buildTime + ? new Date(version.buildTime).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + : null; + return ( <>
- © {year} Jason Stedwell - · + © {year} Jason Stedwell + · - · + · cpas + {sha && sha !== 'dev' && ( + <> + · + + {sha} + + + )}
); @@ -129,6 +148,14 @@ const sf = { export default function App() { const [tab, setTab] = useState('dashboard'); const [showReadme, setShowReadme] = useState(false); + const [version, setVersion] = useState(null); + + useEffect(() => { + fetch('/version.json') + .then(r => r.ok ? r.json() : null) + .then(v => { if (v) setVersion(v); }) + .catch(() => {}); + }, []); return ( @@ -156,7 +183,7 @@ export default function App() {
- + {showReadme && setShowReadme(false)} />} -- 2.52.0 From 602f371d67c90e684e6cfa04e310a85a0e431d91 Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 8 Mar 2026 22:02:10 -0500 Subject: [PATCH 19/40] Add mobile-responsive CSS utility file --- client/src/styles/mobile.css | 113 +++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 client/src/styles/mobile.css diff --git a/client/src/styles/mobile.css b/client/src/styles/mobile.css new file mode 100644 index 0000000..89c02f4 --- /dev/null +++ b/client/src/styles/mobile.css @@ -0,0 +1,113 @@ +/* Mobile-Responsive Utilities for CPAS Tracker */ +/* Target: Standard phones 375px+ with graceful degradation */ + +/* Base responsive utilities */ +@media (max-width: 768px) { + /* Hide scrollbars but keep functionality */ + * { + -webkit-overflow-scrolling: touch; + } + + /* Touch-friendly tap targets (min 44px) */ + button, a, input, select { + min-height: 44px; + } + + /* Improve form input sizing on mobile */ + input, select, textarea { + font-size: 16px !important; /* Prevents iOS zoom on focus */ + } +} + +/* Tablet and below */ +@media (max-width: 1024px) { + .hide-tablet { + display: none !important; + } +} + +/* Mobile portrait and landscape */ +@media (max-width: 768px) { + .hide-mobile { + display: none !important; + } + + .mobile-full-width { + width: 100% !important; + } + + .mobile-text-center { + text-align: center !important; + } + + .mobile-no-padding { + padding: 0 !important; + } + + .mobile-small-padding { + padding: 12px !important; + } + + /* Stack flex containers vertically */ + .mobile-stack { + flex-direction: column !important; + } + + /* Allow horizontal scroll for tables */ + .mobile-scroll-x { + overflow-x: auto !important; + -webkit-overflow-scrolling: touch; + } + + /* Card-based layout helpers */ + .mobile-card { + display: block !important; + padding: 16px; + margin-bottom: 12px; + border-radius: 8px; + background: #181924; + border: 1px solid #2a2b3a; + } + + .mobile-card-row { + display: flex; + justify-content: space-between; + padding: 8px 0; + border-bottom: 1px solid #1c1d29; + } + + .mobile-card-row:last-child { + border-bottom: none; + } + + .mobile-card-label { + font-weight: 600; + color: #9ca0b8; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .mobile-card-value { + font-weight: 600; + color: #f8f9fa; + text-align: right; + } +} + +/* Small mobile phones */ +@media (max-width: 480px) { + .hide-small-mobile { + display: none !important; + } +} + +/* Utility for sticky positioning on mobile */ +@media (max-width: 768px) { + .mobile-sticky-top { + position: sticky; + top: 0; + z-index: 100; + background: #000000; + } +} -- 2.52.0 From 74492142a14a15b1d1eb810634b164119f67d051 Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 8 Mar 2026 22:04:05 -0500 Subject: [PATCH 20/40] Update App.jsx with mobile-responsive navigation and layout --- client/src/App.jsx | 105 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 93 insertions(+), 12 deletions(-) diff --git a/client/src/App.jsx b/client/src/App.jsx index 0055663..2b02499 100755 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -3,6 +3,7 @@ import ViolationForm from './components/ViolationForm'; import Dashboard from './components/Dashboard'; import ReadmeModal from './components/ReadmeModal'; import ToastProvider from './components/ToastProvider'; +import './styles/mobile.css'; const REPO_URL = 'https://git.alwisp.com/jason/cpas'; const PROJECT_START = new Date('2026-03-06T11:33:32-06:00'); @@ -56,8 +57,19 @@ function AppFooter({ version }) { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(0.75); } } + + /* Mobile-specific footer adjustments */ + @media (max-width: 768px) { + .footer-content { + flex-wrap: wrap; + justify-content: center; + font-size: 10px; + padding: 10px 16px; + gap: 8px; + } + } `} -