From b5a588e75255f330447d0b953922c7dac9d061d2 Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 7 Mar 2026 18:31:14 -0600 Subject: [PATCH 1/3] =?UTF-8?q?docs:=20update=20README=20=E2=80=94=20add?= =?UTF-8?q?=20notes/expiration=20features,=20new=20endpoints,=20updated=20?= =?UTF-8?q?schema=20and=20roadmap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 91 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 8a9d576..ee3a454 100755 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ docker run -d --name cpas-tracker -p 3001:3001 -v cpas-data:/data cpas-tracker - **At-risk badge**: flags employees within 2 points of the next tier escalation - Search/filter by name, department, or supervisor - 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 @@ -68,12 +68,14 @@ docker run -d --name cpas-tracker -p 3001:3001 -v cpas-data:/data cpas-tracker ### Employee Profile Modal - Full violation history with resolution status and **amendment count badge** per record -- **โœŽ Edit Employee** button โ€” update name, department, or supervisor inline +- **โœŽ Edit Employee** button โ€” update name, department, supervisor, or notes inline - **Merge Duplicate** tab โ€” reassign all violations from a duplicate record and delete it - **Amend** button per active violation โ€” edit non-scoring fields (location, notes, witness, etc.) with a full field-level diff history - Negate / restore individual violations (soft delete with resolution type + notes) - Hard delete option for data entry errors - PDF download for any historical violation record +- **Notes & Flags** โ€” free-text notes (e.g. "on PIP", "union member") with quick-add tag buttons; visible in the profile modal without affecting scoring +- **Point Expiration Timeline** โ€” shows when each active violation rolls off the 90-day window, with a progress bar, days-remaining countdown, and projected tier-drop indicators ### Audit Log - Append-only log of every write action: employee created/edited/merged, violation logged/amended/negated/restored/deleted @@ -85,6 +87,11 @@ docker run -d --name cpas-tracker -p 3001:3001 -v cpas-data:/data cpas-tracker - Point values, violation type, and incident date are immutable - Every change is stored as a field-level diff (old โ†’ new value) with timestamp and actor +### In-App Documentation +- **? Docs** button in the navbar opens a slide-in admin reference panel +- Covers feature map, CPAS tier system, workflow guidance, and roadmap +- No external link required; always reflects current deployed version + ### CPAS Tier System | Points | Tier | Label | @@ -112,21 +119,23 @@ Scores are computed over a **rolling 90-day window** (negated violations exclude | Method | Endpoint | Description | |--------|----------|-------------| | GET | `/api/health` | Health check | -| GET | `/api/employees` | List all employees | +| GET | `/api/employees` | List all employees (includes `notes`) | | POST | `/api/employees` | Create or upsert employee | -| PATCH | `/api/employees/:id` | Edit employee name, department, or supervisor | -| POST | `/api/employees/:id/merge` | Merge duplicate employee into target; reassigns all violations | +| PATCH | `/api/employees/:id` | Edit name, department, supervisor, or notes | +| POST | `/api/employees/:id/merge` | Merge duplicate employee; reassigns all violations | | GET | `/api/employees/:id/score` | Get active CPAS score for employee | +| GET | `/api/employees/:id/expiration` | Active violation roll-off timeline with days remaining | +| PATCH | `/api/employees/:id/notes` | Save employee notes only (shorthand) | | GET | `/api/dashboard` | All employees with active points + violation counts | | POST | `/api/violations` | Log a new violation | -| GET | `/api/violations/employee/:id` | Get violation history for employee (with resolutions + amendment counts) | +| 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/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 | | DELETE | `/api/violations/:id` | Hard delete a violation | | GET | `/api/violations/:id/pdf` | Download violation PDF | -| GET | `/api/audit` | Paginated audit log (filterable by entity_type, entity_id) | +| GET | `/api/audit` | Paginated audit log (filterable by `entity_type`, `entity_id`) | --- @@ -134,16 +143,16 @@ 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 -โ”‚ โ””โ”€โ”€ database.js # SQLite connection (better-sqlite3) + auto-migrations +โ”‚ โ”œโ”€โ”€ schema.sql # Tables + 90-day active score view +โ”‚ โ””โ”€โ”€ database.js # SQLite connection (better-sqlite3) + auto-migrations โ”œโ”€โ”€ pdf/ -โ”‚ โ””โ”€โ”€ generator.js # Puppeteer PDF generation -โ””โ”€โ”€ client/ # React frontend (Vite) +โ”‚ โ””โ”€โ”€ generator.js # Puppeteer PDF generation +โ””โ”€โ”€ client/ # React frontend (Vite) โ”œโ”€โ”€ package.json โ”œโ”€โ”€ vite.config.js โ”œโ”€โ”€ index.html @@ -151,20 +160,23 @@ cpas/ โ”œโ”€โ”€ main.jsx โ”œโ”€โ”€ App.jsx โ”œโ”€โ”€ data/ - โ”‚ โ””โ”€โ”€ violations.js # All CPAS violation definitions + groups + โ”‚ โ””โ”€โ”€ violations.js # All CPAS violation definitions + groups โ”œโ”€โ”€ hooks/ โ”‚ โ””โ”€โ”€ 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 - โ”œโ”€โ”€ 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 + โ”œโ”€โ”€ 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 + โ”œโ”€โ”€ 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 + โ””โ”€โ”€ ReadmeModal.jsx # In-app admin documentation panel ``` --- @@ -173,7 +185,7 @@ cpas/ Six tables + one view: -- **`employees`** โ€” id, name, department, supervisor +- **`employees`** โ€” id, name, department, supervisor, **notes** - **`violations`** โ€” full incident record including `prior_active_points` snapshot at time of logging - **`violation_resolutions`** โ€” resolution type, details, resolved_by (linked to violations) - **`violation_amendments`** โ€” field-level diff log for violation edits; one row per changed field per amendment @@ -182,6 +194,20 @@ Six tables + one view: --- +## Amendable Fields + +Point values, violation type, and incident date are **immutable** after submission. The following fields can be amended: + +| Field | Notes | +|-------|-------| +| `incident_time` | Time of day the incident occurred | +| `location` | Where the incident took place | +| `details` | Narrative description | +| `submitted_by` | Supervisor who submitted | +| `witness_name` | Witness on record | + +--- + ## Roadmap ### โœ… Completed @@ -204,18 +230,13 @@ Six tables + one view: | 5 | Employee edit / merge | Update employee name/dept/supervisor; merge duplicate records without losing history | | 5 | Violation amendment | Edit non-scoring fields with field-level audit trail | | 5 | Audit log | Append-only log of all system writes; filterable panel in the dashboard | +| 6 | Employee notes / flags | Free-text notes on employee record with quick-add HR tags; does not affect scoring | +| 6 | Point expiration timeline | Per-violation roll-off countdown with tier-drop projections | +| 6 | In-app documentation | Admin usage guide and feature map accessible from the navbar | --- -### ๐Ÿ“‹ In Progress - -#### Reporting & Visibility -- **Expiration timeline** โ€” per-employee view showing which active violations roll off the 90-day window and when; lets supervisors anticipate tier drops before they happen -- **Employee notes / flags** โ€” free-text notes attached to an employee record (e.g. "on PIP", "union member") visible in the profile modal without affecting scoring - ---- - -### ๐Ÿ’ก Proposed +### ๐Ÿ“‹ Proposed #### 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 -- 2.49.1 From db3470099699af6eae1f996363a80c976d40f678 Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 7 Mar 2026 18:33:50 -0600 Subject: [PATCH 2/3] =?UTF-8?q?docs:=20sync=20README=20=E2=80=94=20add=20i?= =?UTF-8?q?n-app=20docs=20to=20features=20+=20completed=20roadmap=20phase?= =?UTF-8?q?=206?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index ee3a454..5b16a3d 100755 --- a/README.md +++ b/README.md @@ -143,16 +143,16 @@ 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 -โ”‚ โ””โ”€โ”€ database.js # SQLite connection (better-sqlite3) + auto-migrations +โ”‚ โ”œโ”€โ”€ schema.sql # Tables + 90-day active score view +โ”‚ โ””โ”€โ”€ database.js # SQLite connection (better-sqlite3) + auto-migrations โ”œโ”€โ”€ pdf/ -โ”‚ โ””โ”€โ”€ generator.js # Puppeteer PDF generation -โ””โ”€โ”€ client/ # React frontend (Vite) +โ”‚ โ””โ”€โ”€ generator.js # Puppeteer PDF generation +โ””โ”€โ”€ client/ # React frontend (Vite) โ”œโ”€โ”€ package.json โ”œโ”€โ”€ vite.config.js โ”œโ”€โ”€ index.html @@ -160,23 +160,23 @@ cpas/ โ”œโ”€โ”€ main.jsx โ”œโ”€โ”€ App.jsx โ”œโ”€โ”€ 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 - โ”œโ”€โ”€ 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 - โ””โ”€โ”€ 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 + โ”œโ”€โ”€ 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 + โ””โ”€โ”€ ReadmeModal.jsx # In-app admin documentation panel ``` --- -- 2.49.1 From 84f51248506ad72aabae53f5c76f9ab8d7883c08 Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 7 Mar 2026 18:33:54 -0600 Subject: [PATCH 3/3] =?UTF-8?q?docs:=20rewrite=20ReadmeModal=20as=20admin?= =?UTF-8?q?=20usage=20guide=20=E2=80=94=20feature=20map,=20workflow,=20tie?= =?UTF-8?q?r=20system,=20roadmap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/ReadmeModal.jsx | 567 ++++++++++++-------------- 1 file changed, 250 insertions(+), 317 deletions(-) diff --git a/client/src/components/ReadmeModal.jsx b/client/src/components/ReadmeModal.jsx index e2c4588..edadd0a 100644 --- a/client/src/components/ReadmeModal.jsx +++ b/client/src/components/ReadmeModal.jsx @@ -1,286 +1,145 @@ import React, { useEffect, useRef } from 'react'; // โ”€โ”€โ”€ Minimal Markdown โ†’ HTML renderer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -// Handles: headings, bold, inline-code, fenced code blocks, tables, hr, -// unordered lists, ordered lists, and paragraphs. function mdToHtml(md) { - const lines = md.split('\n'); - const out = []; - let i = 0; - let inUl = false; - let inOl = false; - let inTable = false; - let tableHead = false; + const lines = md.split('\n'); + const out = []; + let i = 0, inUl = false, inOl = false, inTable = false; - const closeOpenLists = () => { - if (inUl) { out.push(''); inUl = false; } - if (inOl) { out.push(''); inOl = false; } - if (inTable) { out.push(''); inTable = false; tableHead = false; } + const close = () => { + 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') - .replace(/`([^`]+)`/g, '$1'); + const inline = s => + s.replace(/&/g,'&').replace(//g,'>') + .replace(/\*\*(.+?)\*\*/g,'$1') + .replace(/`([^`]+)`/g,'$1'); while (i < lines.length) { const line = lines[i]; - // Fenced code block if (line.startsWith('```')) { - closeOpenLists(); - const lang = line.slice(3).trim(); - const codeLines = []; + close(); i++; - while (i < lines.length && !lines[i].startsWith('```')) { - codeLines.push(lines[i].replace(/&/g,'&').replace(//g,'>')); - i++; - } - out.push(`
${codeLines.join('\n')}
`); - i++; - continue; + 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; } - // HR - if (/^---+$/.test(line.trim())) { - closeOpenLists(); - out.push('
'); - i++; - continue; - } - - // Headings - const hMatch = line.match(/^(#{1,4})\s+(.+)/); - if (hMatch) { - closeOpenLists(); - const level = hMatch[1].length; - const id = hMatch[2].toLowerCase().replace(/[^a-z0-9]+/g, '-'); - out.push(`${inline(hMatch[2])}`); - i++; - continue; - } - - // Table row if (line.trim().startsWith('|')) { - const cells = line.trim().replace(/^\||\|$/g, '').split('|').map(c => c.trim()); + const cells = line.trim().replace(/^\||\|$/g,'').split('|').map(c=>c.trim()); if (!inTable) { - closeOpenLists(); - inTable = true; - tableHead = true; + close(); inTable = true; out.push(''); cells.forEach(c => out.push(``)); out.push(''); i++; - // skip separator row - if (i < lines.length && lines[i].trim().startsWith('|') && /^[\|\s\-:]+$/.test(lines[i])) i++; + if (i < lines.length && /^[\|\s\-:]+$/.test(lines[i])) i++; continue; } else { out.push(''); cells.forEach(c => out.push(``)); out.push(''); - i++; - continue; + i++; continue; } } - // Unordered list - const ulMatch = line.match(/^[-*]\s+(.*)/); - if (ulMatch) { - if (inTable) closeOpenLists(); - if (!inUl) { if (inOl) { out.push(''); inOl = false; } out.push('
    '); inUl = true; } - out.push(`
  • ${inline(ulMatch[1])}
  • `); - 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; } - // Ordered list - const olMatch = line.match(/^\d+\.\s+(.*)/); - if (olMatch) { - if (inTable) closeOpenLists(); - if (!inOl) { if (inUl) { out.push('
    '); inUl = false; } out.push('
      '); inOl = true; } - out.push(`
    1. ${inline(olMatch[1])}
    2. `); - 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(`
  1. ${inline(ol[1])}
  2. `); + i++; continue; } - // Blank line - if (line.trim() === '') { - closeOpenLists(); - i++; - continue; - } + if (line.trim() === '') { close(); i++; continue; } - // Paragraph - closeOpenLists(); + close(); out.push(`

    ${inline(line)}

    `); i++; } - - closeOpenLists(); + close(); return out.join('\n'); } // โ”€โ”€โ”€ Styles โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -const overlay = { - position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.75)', - zIndex: 2000, display: 'flex', alignItems: 'flex-start', justifyContent: 'flex-end', +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' }, }; -const panel = { - background: '#111217', color: '#f8f9fa', width: '760px', maxWidth: '95vw', - height: '100vh', overflowY: 'auto', boxShadow: '-4px 0 32px rgba(0,0,0,0.8)', - display: 'flex', flexDirection: 'column', -}; - -const 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', -}; - -const closeBtn = { - background: 'none', border: 'none', color: 'white', - fontSize: '22px', cursor: 'pointer', lineHeight: 1, -}; - -const body = { - padding: '28px 32px', flex: 1, fontSize: '13px', lineHeight: '1.7', -}; - -// Injected -
    e.stopPropagation()}> +
    e.stopPropagation()}> {/* Header */} -
    +
    -
    - ๐Ÿ“‹ CPAS Tracker โ€” Documentation +
    + ๐Ÿ“‹ CPAS Tracker โ€” Admin Guide
    -
    - Admin reference ยท use Esc or click outside to close +
    + Feature map ยท workflow reference ยท roadmap ยท Esc or click outside to close
    - +
    {/* TOC strip */} -
    - {toc.filter(h => h.level <= 2).map((h) => ( +
    + {toc.map(h => (
${inline(c)}
${inline(c)}