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(`| ${inline(c)} | `));
- out.push('
');
- i++;
- if (i < lines.length && /^[\|\s\-:]+$/.test(lines[i])) i++;
- continue;
- } else {
- out.push('');
- cells.forEach(c => out.push(`| ${inline(c)} | `));
- out.push('
');
- i++; continue;
- }
+ if (!inTable) { close(); inTable=true; out.push(''); cells.forEach(c=>out.push(`| ${inline(c)} | `)); out.push('
'); i++; if (i < lines.length && /^[\|\s\-:]+$/.test(lines[i])) i++; continue; }
+ else { out.push(''); cells.forEach(c=>out.push(`| ${inline(c)} | `)); 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=true; }
- out.push(`- ${inline(ul[1])}
`);
- i++; continue;
- }
-
+ if (ul) { if (inTable) close(); if (!inUl) { if (inOl){out.push('');inOl=false;} out.push('');inUl=true; } out.push(`- ${inline(ul[1])}
`); i++; continue; }
const ol = line.match(/^\d+\.\s+(.*)/);
- if (ol) {
- if (inTable) close();
- if (!inOl) { if (inUl) { out.push('
'); inUl=false; } out.push(''); inOl=true; }
- out.push(`- ${inline(ol[1])}
`);
- i++; continue;
- }
-
+ if (ol) { if (inTable) close(); if (!inOl) { if (inUl){out.push('
');inUl=false;} out.push('');inOl=true; } out.push(`- ${inline(ol[1])}
`); 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 => (
-