From 281825377f8c4ed3af70d2bd7dccb3a2c899d428 Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 7 Mar 2026 18:39:01 -0600 Subject: [PATCH] =?UTF-8?q?feat:=20ReadmeModal=20=E2=80=94=20admin=20usage?= =?UTF-8?q?=20guide,=20feature=20map,=20workflow=20reference,=20roadmap=20?= =?UTF-8?q?(no=20install=20content)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/ReadmeModal.jsx | 177 +++++++++----------------- 1 file changed, 57 insertions(+), 120 deletions(-) diff --git a/client/src/components/ReadmeModal.jsx b/client/src/components/ReadmeModal.jsx index edadd0a..8819b89 100644 --- a/client/src/components/ReadmeModal.jsx +++ b/client/src/components/ReadmeModal.jsx @@ -1,17 +1,16 @@ import React, { useEffect, useRef } from 'react'; -// ─── Minimal Markdown → HTML renderer ──────────────────────────────────────── +// Minimal Markdown to HTML renderer (headings, bold, inline-code, tables, hr, ul, ol, paragraphs) function mdToHtml(md) { const lines = md.split('\n'); const out = []; let i = 0, inUl = false, inOl = false, inTable = false; const close = () => { - if (inUl) { out.push(''); inUl = false; } - if (inOl) { out.push(''); inOl = false; } - if (inTable) { out.push(''); inTable = false; } + if (inUl) { out.push(''); inUl = false; } + if (inOl) { out.push(''); inOl = false; } + if (inTable) { out.push(''); inTable = false; } }; - const inline = s => s.replace(/&/g,'&').replace(//g,'>') .replace(/\*\*(.+?)\*\*/g,'$1') @@ -19,121 +18,72 @@ function mdToHtml(md) { while (i < lines.length) { const line = lines[i]; - - if (line.startsWith('```')) { - close(); - i++; - while (i < lines.length && !lines[i].startsWith('```')) i++; - i++; continue; - } + if (line.startsWith('```')) { close(); i++; while (i < lines.length && !lines[i].startsWith('```')) i++; i++; continue; } if (/^---+$/.test(line.trim())) { close(); out.push('
'); i++; continue; } - const hm = line.match(/^(#{1,4})\s+(.+)/); - if (hm) { - close(); - const lvl = hm[1].length; - const id = hm[2].toLowerCase().replace(/[^a-z0-9]+/g,'-'); - out.push(`${inline(hm[2])}`); - i++; continue; - } - + if (hm) { close(); const lv=hm[1].length, id=hm[2].toLowerCase().replace(/[^a-z0-9]+/g,'-'); out.push(`${inline(hm[2])}`); i++; continue; } if (line.trim().startsWith('|')) { const cells = line.trim().replace(/^\||\|$/g,'').split('|').map(c=>c.trim()); - if (!inTable) { - close(); inTable = true; - out.push(''); - cells.forEach(c => out.push(``)); - out.push(''); - i++; - if (i < lines.length && /^[\|\s\-:]+$/.test(lines[i])) i++; - continue; - } else { - out.push(''); - cells.forEach(c => out.push(``)); - out.push(''); - i++; continue; - } + if (!inTable) { close(); inTable=true; out.push('
${inline(c)}
${inline(c)}
'); cells.forEach(c=>out.push(``)); out.push(''); i++; if (i < lines.length && /^[\|\s\-:]+$/.test(lines[i])) i++; continue; } + else { out.push(''); cells.forEach(c=>out.push(``)); out.push(''); i++; continue; } } - const ul = line.match(/^[-*]\s+(.*)/); - if (ul) { - if (inTable) close(); - if (!inUl) { if (inOl) { out.push(''); inOl=false; } out.push('');inUl=false;} out.push('
    ');inOl=true; } out.push(`
  1. ${inline(ol[1])}
  2. `); i++; continue; } if (line.trim() === '') { close(); i++; continue; } - - close(); - out.push(`

    ${inline(line)}

    `); - i++; + close(); out.push(`

    ${inline(line)}

    `); i++; } close(); return out.join('\n'); } +function buildToc(md) { + return md.split('\n').reduce((acc, line) => { + const m = line.match(/^(#{1,2})\s+(.+)/); + if (m) acc.push({ level: m[1].length, text: m[2], id: m[2].toLowerCase().replace(/[^a-z0-9]+/g,'-') }); + return acc; + }, []); +} + // ─── Styles ─────────────────────────────────────────────────────────────────── const S = { - overlay: { - position:'fixed', inset:0, background:'rgba(0,0,0,0.75)', - zIndex:2000, display:'flex', alignItems:'flex-start', justifyContent:'flex-end', - }, - panel: { - background:'#111217', color:'#f8f9fa', width:'780px', maxWidth:'95vw', - height:'100vh', overflowY:'auto', boxShadow:'-4px 0 32px rgba(0,0,0,0.85)', - display:'flex', flexDirection:'column', - }, - header: { - background:'linear-gradient(135deg,#000000,#151622)', color:'white', - padding:'22px 28px', position:'sticky', top:0, zIndex:10, - borderBottom:'1px solid #222', display:'flex', alignItems:'center', - justifyContent:'space-between', - }, - closeBtn: { background:'none', border:'none', color:'white', fontSize:'22px', cursor:'pointer', lineHeight:1 }, - toc: { - background:'#0d1117', borderBottom:'1px solid #1e1f2e', - padding:'10px 32px', display:'flex', flexWrap:'wrap', gap:'4px 18px', fontSize:'11px', - }, - body: { padding:'28px 32px', flex:1, fontSize:'13px', lineHeight:'1.75' }, - footer: { padding:'14px 32px', borderTop:'1px solid #1e1f2e', fontSize:'11px', color:'#555770', textAlign:'center' }, + overlay: { position:'fixed', inset:0, background:'rgba(0,0,0,0.75)', zIndex:2000, display:'flex', alignItems:'flex-start', justifyContent:'flex-end' }, + panel: { background:'#111217', color:'#f8f9fa', width:'760px', maxWidth:'95vw', height:'100vh', overflowY:'auto', boxShadow:'-4px 0 32px rgba(0,0,0,0.85)', display:'flex', flexDirection:'column' }, + header: { background:'linear-gradient(135deg,#000000,#151622)', color:'white', padding:'22px 28px', position:'sticky', top:0, zIndex:10, borderBottom:'1px solid #222', display:'flex', alignItems:'center', justifyContent:'space-between' }, + closeBtn:{ background:'none', border:'none', color:'white', fontSize:'22px', cursor:'pointer', lineHeight:1 }, + toc: { background:'#0d1117', borderBottom:'1px solid #1e1f2e', padding:'10px 32px', display:'flex', flexWrap:'wrap', gap:'4px 18px', fontSize:'11px' }, + body: { padding:'28px 32px', flex:1, fontSize:'13px', lineHeight:'1.75' }, + footer: { padding:'14px 32px', borderTop:'1px solid #1e1f2e', fontSize:'11px', color:'#555770', textAlign:'center' }, }; const CSS = ` -.adm h1 { font-size:21px; font-weight:800; color:#f8f9fa; margin:28px 0 10px; border-bottom:1px solid #2a2b3a; padding-bottom:8px; } -.adm h2 { font-size:16px; font-weight:700; color:#d4af37; margin:28px 0 6px; letter-spacing:.2px; } -.adm h3 { font-size:12px; font-weight:700; color:#90caf9; margin:18px 0 4px; text-transform:uppercase; letter-spacing:.5px; } -.adm h4 { font-size:13px; font-weight:600; color:#b0b8d0; margin:14px 0 4px; } -.adm p { color:#c8ccd8; margin:5px 0 10px; } -.adm hr { border:none; border-top:1px solid #2a2b3a; margin:22px 0; } -.adm strong { color:#f8f9fa; } -.adm code { background:#0d1117; color:#79c0ff; border:1px solid #2a2b3a; border-radius:4px; padding:1px 6px; font-family:'Consolas','Fira Code',monospace; font-size:12px; } -.adm ul { padding-left:20px; margin:5px 0 10px; color:#c8ccd8; } -.adm ol { padding-left:20px; margin:5px 0 10px; color:#c8ccd8; } -.adm li { margin:4px 0; } -.adm table { width:100%; border-collapse:collapse; font-size:12px; background:#181924; border-radius:6px; overflow:hidden; border:1px solid #2a2b3a; margin:10px 0 16px; } -.adm th { background:#050608; padding:8px 12px; text-align:left; color:#f8f9fa; font-weight:600; font-size:11px; text-transform:uppercase; border-bottom:1px solid #2a2b3a; } -.adm td { padding:8px 12px; border-bottom:1px solid #202231; color:#c8ccd8; } -.adm tr:last-child td { border-bottom:none; } -.adm tr:hover td { background:#1e1f2e; } +.adm h1 { font-size:21px; font-weight:800; color:#f8f9fa; margin:28px 0 10px; border-bottom:1px solid #2a2b3a; padding-bottom:8px } +.adm h2 { font-size:16px; font-weight:700; color:#d4af37; margin:28px 0 6px; letter-spacing:.2px } +.adm h3 { font-size:12px; font-weight:700; color:#90caf9; margin:18px 0 4px; text-transform:uppercase; letter-spacing:.5px } +.adm h4 { font-size:13px; font-weight:600; color:#b0b8d0; margin:14px 0 4px } +.adm p { color:#c8ccd8; margin:5px 0 10px } +.adm hr { border:none; border-top:1px solid #2a2b3a; margin:22px 0 } +.adm strong { color:#f8f9fa } +.adm code { background:#0d1117; color:#79c0ff; border:1px solid #2a2b3a; border-radius:4px; padding:1px 6px; font-family:'Consolas','Fira Code',monospace; font-size:12px } +.adm ul { padding-left:20px; margin:5px 0 10px; color:#c8ccd8 } +.adm ol { padding-left:20px; margin:5px 0 10px; color:#c8ccd8 } +.adm li { margin:4px 0 } +.adm table { width:100%; border-collapse:collapse; font-size:12px; background:#181924; border-radius:6px; overflow:hidden; border:1px solid #2a2b3a; margin:10px 0 16px } +.adm th { background:#050608; padding:8px 12px; text-align:left; color:#f8f9fa; font-weight:600; font-size:11px; text-transform:uppercase; border-bottom:1px solid #2a2b3a } +.adm td { padding:8px 12px; border-bottom:1px solid #202231; color:#c8ccd8 } +.adm tr:last-child td { border-bottom:none } +.adm tr:hover td { background:#1e1f2e } `; -// ─── Admin guide content ────────────────────────────────────────────────────── +// ─── Admin guide content (no install / Docker content) ──────────────────────── const GUIDE_MD = `# CPAS Tracker — Admin Guide Internal tool for CPAS violation documentation, workforce standing management, and audit compliance. All data is stored locally in the Docker container volume — there is no external dependency. --- -## How CPAS Scoring Works +## How Scoring Works Every violation carries a **point value** set at the time of submission. Points count toward an employee's score only within a **rolling 90-day window** — once a violation is older than 90 days it automatically drops off and the score recalculates. @@ -161,7 +111,7 @@ The **at-risk badge** on the dashboard flags anyone within 2 points of the next The main view. Employees are sorted by active CPAS points, highest first. -- **Stat cards** — live counts: total employees, zero-point (elite), active points, at-risk, highest score +- **Stat cards** — live counts: total employees, zero-point (elite), with active points, at-risk, highest score - **Search / filter** — by name, department, or supervisor; narrows the table in real time - **At-risk badge** — gold flag on rows where the employee is within 2 pts of the next tier - **Audit Log button** — opens the filterable, paginated write-action log (top right of the dashboard toolbar) @@ -254,7 +204,7 @@ Each amendment stores a before/after diff for every changed field. Amendment his ## Roadmap -### ✅ Shipped +### Shipped - Container scaffold, violation form, employee intelligence - Recidivist auto-escalation, tier crossing warning @@ -270,7 +220,7 @@ Each amendment stores a before/after diff for every changed field. Amendment his --- -### 🔜 Near-term +### Near-term These are well-scoped additions that fit the current architecture without major changes. @@ -280,7 +230,7 @@ These are well-scoped additions that fit the current architecture without major --- -### 📋 Planned +### Planned Larger features that require more design work or infrastructure. @@ -291,27 +241,18 @@ Larger features that require more design work or infrastructure. --- -### 🔭 Future Considerations +### Future Considerations These require meaningful infrastructure additions and should be evaluated against actual operational need before committing. - **Multi-user auth** — role-based login (admin, supervisor, read-only). Currently the app assumes a trusted internal network with no authentication layer. - **Tier escalation alerts** — email or in-app notification when an employee crosses into Tier 2+, automatically routed to their supervisor. - **Scheduled digest** — weekly email summary to supervisors showing their employees' current standings and any approaching thresholds. -- **Automated DB backup** — scheduled snapshot of `/data/cpas.db` to a mounted backup volume or remote destination. +- **Automated DB backup** — scheduled snapshot of the database to a mounted backup volume or remote destination. - **Bulk CSV import** — migrate historical violation records from paper logs or a prior system. - **Dark/light theme toggle** — UI is currently dark-only. `; -// ─── TOC builder ───────────────────────────────────────────────────────────── -function buildToc(md) { - return md.split('\n').reduce((acc, line) => { - const m = line.match(/^(#{1,2})\s+(.+)/); - if (m) acc.push({ level: m[1].length, text: m[2], id: m[2].toLowerCase().replace(/[^a-z0-9]+/g,'-') }); - return acc; - }, []); -} - // ─── Component ──────────────────────────────────────────────────────────────── export default function ReadmeModal({ onClose }) { const bodyRef = useRef(null); @@ -342,7 +283,7 @@ export default function ReadmeModal({ onClose }) { 📋 CPAS Tracker — Admin Guide
    - Feature map · workflow reference · roadmap · Esc or click outside to close + Feature map · workflows · roadmap · Esc or click outside to close
    @@ -351,16 +292,12 @@ export default function ReadmeModal({ onClose }) { {/* TOC strip */}
    {toc.map(h => ( - ))}
${inline(c)}
${inline(c)}