From 7e1164af13d93379a2ce8de9dae6103b16a8bf07 Mon Sep 17 00:00:00 2001 From: jason Date: Sun, 8 Mar 2026 00:21:30 -0600 Subject: [PATCH 01/32] 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 02/32] 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 03/32] 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 04/32] 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 05/32] 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 06/32] 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 07/32] 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 08/32] 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 09/32] 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 10/32] 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 11/32] 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 12/32] 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; + } + } `} -