From 981fa3bea44cb8c79cb08976ad2de320788c53bb Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 8 Mar 2026 00:35:35 -0600 Subject: [PATCH 01/29] 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 02/29] 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 03/29] 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 04/29] 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 05/29] 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 06/29] 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 07/29] 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 08/29] 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 09/29] 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; + } + } `} -