Files
Project-SDE-WP-Suite/html/admin.html
Matt Mabrey a6c3fbfe50 T11.5: admin console Activity & usage card (CR-019)
New card in admin.html/admin.js, above the old per-browser Usage logs
card (which T11.6 retires next). Filters (date range, project, user,
tool) drive GET /api/usage/summary; two export buttons call
GET /api/usage/export (raw / sanitized) and save the CSV via a Blob,
same download pattern wp-usage.js already uses.

Access: the card lives inside admin.html, already gated admin-only
client-side by gateByRole() (unchanged) - a non-admin never sees the
card. The underlying API is gated server-side by require_user_manager
regardless (admin or project_super_user on >=1 project), independent
of and stricter than the client-side gate, so a non-admin request is
refused even if someone reached the endpoint directly.

Accessibility (C1): every control is a real <input>/<select>/<button>,
keyboard-operable. Status/export banners use the existing '.banner' /
'*-banner' id convention, which console-util.js's MutationObserver
already turns into aria-live (role=status, or role=alert on a '.bad'
banner) - no new announcement plumbing needed. No new CSS: reuses
.toolbar/.banner/.card/.row/.kv/.users, so nothing here adds a second
token source (the token rule).

Verified so far:
  - node --check html/admin.js: no syntax errors
  - every id admin.js's new code references exists in admin.html
    (scripted diff against the full getElementById/id= sets)
  - live-server check: GET /api/usage/summary with the exact
    (possibly-empty) query string _activityFilters() builds returns
    the {active_users, per_user_last_active, by_tool, event_count}
    shape renderActivity() expects; project_id filter narrows
    correctly; GET /api/usage/export?sanitize=true returns
    text/csv with the expected header row
  - full smoke test + seed_demo.py still pass

NOT yet verified: rendering at 390px/1440px with before/after
screenshots (CLAUDE.md verification step). This sandbox has no
headless-capable browser (no chromium/msedge on PATH) and the
playwright/chromium download is blocked by this environment's
network allowlist, so tests/cdp.py's harness can't run here. Deferred
to T11.7, same as wave 10's browser checks — flagging rather than
skipping silently.
2026-09-23 12:31:55 -07:00

264 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Console — Work Package Suite</title>
<script src="auth-guard.js"></script>
<!-- Date/number formatting. Must parse BEFORE the app scripts: they format
timestamps during their own boot. -->
<script src="wp-format.js"></script>
<!-- Addressable state (S3). Parses before the app scripts, which read the URL
during their own boot. -->
<script src="wp-url.js"></script>
<link rel="icon" href="favicon.ico" sizes="any">
<link rel="manifest" href="manifest.webmanifest">
<meta name="theme-color" content="#161616">
<link rel="stylesheet" href="theme-light.css">
<link rel="stylesheet" href="wp-chrome.css">
<link rel="stylesheet" href="console.css">
<link rel="stylesheet" href="wp-sidenav.css">
<style>
/* Page-specific only — the tokens, cards, controls, tables, banners and modal
live in console.css, shared with the User Directory. What stays here is what
only this page has: the per-card scroll boxes admin.js paints tables into, the
column exceptions for those tables, and the admins-only notice.
These are addressed by ID because admin.js emits the tables without per-cell
classes. */
/* These start life as empty divs that admin.js fills on demand, so they only earn
their gap once they are actually saying something. */
#projects-banner:not(:empty), #defmem-banner:not(:empty){ margin-bottom:var(--s3); }
/* Every container admin.js paints a table into is a scrollport of its own, so a
sticky header always has something to stick to rather than sliding up behind
the app bar. Same rule as console.css's .tscroll. */
#comments-admin, #audit-admin, #notif-box, #usage-admin, #activity-admin, #projects-table, #defmem-table{
overflow:auto; max-height:min(70vh,640px); overscroll-behavior:contain; }
/* If admin.js wraps its table in its own .tscroll, the outer box steps aside so
one table never ends up with two scrollbars. */
#comments-admin:has(.tscroll), #audit-admin:has(.tscroll), #notif-box:has(.tscroll),
#usage-admin:has(.tscroll), #activity-admin:has(.tscroll), #projects-table:has(.tscroll), #defmem-table:has(.tscroll){
overflow:visible; max-height:none; }
/* Comment text and audit detail are the two columns you are actually here to
read, so they wrap inside a sane width instead of truncating. */
#comments-admin table td:nth-child(5){ white-space:normal; min-width:260px; max-width:640px; }
#audit-admin table td:nth-child(6){ white-space:normal; max-width:420px; }
/* The denial notice is a sentence, not a table — don't stretch it to 1240px. */
#admin-denied .card{ max-width:560px; }
.gate-box input{ width:100%; height:var(--ctl); padding:0 var(--s3); font:inherit; font-size:14px;
border:1px solid var(--border-strong); border-radius:0; margin-bottom:var(--s3); }
@media (max-width:900px){
#comments-admin, #audit-admin, #notif-box, #usage-admin, #activity-admin, #projects-table, #defmem-table{
max-height:none; }
}
</style>
</head>
<body>
<!-- SHARED DARK APP BAR -->
<header class="wp-appbar">
<a href="index.html" class="wp-appbar-brand" title="Back to site">
<span class="wp-logo-chip"><img src="prime-controls-logo.jpg" alt="Prime Controls"></span>
<span class="wp-appbar-title">Work Package Suite <span class="wp-appbar-sub">| Admin Console</span></span>
</a>
</header>
<!-- ADMINS ONLY (shown if the signed-in account isn't an admin) -->
<div class="wrap" id="admin-denied" style="display:none">
<div class="card">
<h2>Admins only</h2>
<p class="sub">Your account doesnt have admin access. Sign in with an admin account, or ask an administrator to grant you the admin role.</p>
<div class="row"><a class="home" href="index.html">← Back to site</a> <button onclick="wpLogout()">Sign out</button></div>
</div>
</div>
<!-- CONSOLE -->
<div class="wrap" id="admin-main" style="display:none">
<div class="row" style="justify-content:space-between; margin-bottom:var(--s5)">
<div><h1>Admin Console</h1><div class="sub" style="margin:0">Stack diagnostics &amp; tests · talks to <code>/api</code> on this host</div></div>
<div class="row"><a class="home" href="index.html">← Site</a></div>
</div>
<!-- CONNECTIVITY -->
<div class="card">
<h2>API connectivity</h2>
<div class="toolbar"><button class="primary" onclick="checkHealth()">Check /api/health</button></div>
<div class="banner" id="health-banner"></div>
</div>
<!-- USER ADMINISTRATION — moved out to its own page -->
<div class="card">
<h2>User accounts</h2>
<div class="sub">Login accounts, permissions and project access now live on the
<strong>User Directory</strong> page. They moved because user administration is no longer
admin-only: a <strong>Project Super User</strong> creates and manages the accounts on the
projects they administer, and they must never be sent through this console to do it.</div>
<div class="toolbar"><a class="home" href="users.html"><button class="primary">Open the user directory →</button></a></div>
</div>
<!-- PROJECTS (ARCHIVE / UNARCHIVE) -->
<div class="card">
<h2>Projects</h2>
<div class="sub">Archiving a project hides it from every picker, switcher and search, and freezes it
read-only — nothing is deleted and every work package, SOP and comment is kept exactly as it is.
Unarchive here to bring it back; the project returns unchanged.</div>
<div class="toolbar">
<button onclick="loadProjects()">Refresh projects</button>
<label class="chk"><input type="checkbox" id="proj-show-archived" onchange="renderProjects()"> Show archived</label>
<input id="proj-search" placeholder="Search name / number / client…" oninput="renderProjects()">
</div>
<div id="projects-banner"></div>
<div id="projects-table"><div class="note">Click “Refresh projects” to load.</div></div>
</div>
<!-- DEFAULT MEMBERS ON NEW PROJECTS -->
<div class="card">
<h2>Default members on new projects</h2>
<div class="sub">Everyone flagged here is added automatically to every project created from now on,
with the role chosen here. It does not touch projects that already exist — for those, use
<strong>Project access</strong> on the <a class="home" href="users.html">User Directory</a>.
Administrators are listed with nothing to set: they already reach every project. This card stays
in the console because it is a rule about <em>every</em> future project, including the ones a
Project Super User has no part in — so only an admin sets it.</div>
<div class="toolbar"><button onclick="loadDefaultMembers()">Refresh</button></div>
<div id="defmem-banner"></div>
<div id="defmem-table"><div class="note">Click “Refresh” to load.</div></div>
</div>
<!-- FEATURE FLAGS -->
<div class="card">
<h2>Features</h2>
<div class="sub">Switches that change what the suite offers on every project.</div>
<div id="features-box" class="note">Loading…</div>
</div>
<!-- NOTIFICATIONS / EMAIL -->
<div class="card">
<h2>Notifications &amp; email</h2>
<div class="sub">Email notifications for work-package assignments, and self-service password resets. <strong>Off by default</strong> — turn this on only once SMTP is configured. The SMTP <strong>password</strong> is read from the <code>SMTP_PASSWORD</code> environment variable and is never stored here.</div>
<div id="settings-box" class="note">Loading…</div>
<div id="notif-box" class="note" style="margin-top:14px"></div>
</div>
<!-- ALL FEEDBACK / COMMENTS -->
<div class="card">
<h2>All feedback &amp; comments</h2>
<div class="sub">Every comment submitted across the suite — who wrote it, what they said, and where they were (page &amp; step) when they commented.</div>
<div class="toolbar">
<button onclick="loadComments()">Refresh comments</button>
<select id="cmt-filter" onchange="renderComments()"><option value="">All sources</option></select>
<input id="cmt-search" placeholder="Search text / author…" oninput="renderComments()">
</div>
<div id="comments-admin" class="note">Click refresh to load.</div>
</div>
<!-- ACTIVITY LOG (AUDIT TRAIL) -->
<div class="card">
<h2>Activity log</h2>
<div class="sub">Who changed what, and when — across projects, SOPs, work packages, and user accounts. Stored server-side in the shared database.</div>
<div class="toolbar">
<button onclick="loadAudit()">Refresh</button>
<select id="audit-type" onchange="renderAudit()">
<option value="">All types</option>
<option value="wp">Work packages</option>
<option value="sop">SOPs</option>
<option value="project">Projects</option>
<option value="user">User accounts</option>
</select>
<input id="audit-search" placeholder="Search actor / action / item…" oninput="renderAudit()">
</div>
<div id="audit-admin" class="note">Click refresh to load.</div>
</div>
<!-- ACTIVITY &amp; USAGE (CR-019) -->
<div class="card">
<h2>Activity &amp; usage</h2>
<div class="sub">Who is using the suite, which tools, and how often — recorded server-side on every
sign-in and page open, kept indefinitely. Filter below, or export a CSV: raw (real usernames) for
internal use, or sanitized (each user replaced with a stable, non-reversible id) for feeding into
Power BI or another external reporting tool without carrying real identities.</div>
<div class="toolbar">
<label for="act-from">From</label>
<input type="date" id="act-from" onchange="loadActivity()">
<label for="act-to">To</label>
<input type="date" id="act-to" onchange="loadActivity()">
<select id="act-project" onchange="loadActivity()"><option value="">All projects</option></select>
<input id="act-user" placeholder="Username…" oninput="loadActivity()">
<select id="act-tool" onchange="loadActivity()">
<option value="">All tools</option>
<option value="launcher">Launcher</option>
<option value="wizard">SOP wizard</option>
<option value="creator">Work package creator</option>
<option value="field_view">Field view</option>
<option value="admin">Admin console</option>
<option value="directory">User directory</option>
</select>
<button onclick="loadActivity()">Refresh</button>
</div>
<div id="activity-banner" class="banner" style="display:none"></div>
<div id="activity-admin" class="note">Loading…</div>
<div class="toolbar" style="margin-top:var(--s3)">
<button onclick="exportActivity(false)">Download CSV (raw)</button>
<button onclick="exportActivity(true)">Download CSV (sanitized)</button>
</div>
<div id="activity-export-banner" class="banner" style="display:none"></div>
</div>
<!-- USAGE LOGS (per-browser — retired once the report above is confirmed, T11.6) -->
<div class="card">
<h2>Usage logs</h2>
<div class="sub">Engagement recorded by both tools — the work package creator and the SOP wizard —
sessions, actions and counts, with a download per tool (D5). Note: stored locally per browser,
so this reflects activity on <strong>this</strong> machine.</div>
<div class="toolbar">
<button onclick="loadUsage()">Refresh</button>
</div>
<div id="usage-admin" class="note">Click refresh to load.</div>
</div>
<!-- DB SNAPSHOT -->
<div class="card">
<h2>Database snapshot</h2>
<div class="toolbar"><button onclick="snapshot()">Refresh counts</button></div>
<div id="snapshot-out" class="note">Click refresh to read row counts from SQL via the API.</div>
</div>
<!-- SMOKE TEST -->
<div class="card">
<h2>End-to-end smoke test</h2>
<div class="sub">Creates a throwaway project, exercises the issue gate / status / metrics / comments, then deletes it (cascade). Mirrors <code>server/smoketest.py</code>.</div>
<div class="toolbar"><button class="primary" onclick="runSmokeTest()">Run smoke test</button></div>
<pre class="out" id="smoke-out">Ready.</pre>
</div>
<!-- DEMO DATA -->
<div class="card">
<h2>Demo data</h2>
<div class="sub">Seed a realistic <code>DEMO</code> project (SOP + a spread of Work Packages) into SQL, or remove all <code>DEMO-</code>/<code>SMOKE-</code> projects.</div>
<div class="toolbar">
<button class="primary" onclick="seedDemo()">Seed demo project</button>
<button class="danger" onclick="cleanDemo()">Clean DEMO / SMOKE projects</button>
</div>
<pre class="out" id="demo-out">Ready.</pre>
<div class="note">Note: the seeded <strong>project</strong> appears in the home picker; its SOP/WPs live in SQL but won't render in the Creator/Dashboard until the front end is wired to the API (Phase 2).</div>
</div>
</div>
<script src="wp-usage.js"></script>
<script src="console-util.js"></script>
<script src="wp-dialog.js"></script>
<script src="admin.js"></script>
<!-- The app bar's project switcher reads ProjectData; without this the bar on this
page could never show a project and always read "Select a project" (F1). Must
parse before wp-chrome.js, which reads it as it mounts. -->
<script src="project-data.js"></script>
<script src="wp-chrome.js"></script>
<script src="wp-sidenav.js"></script>
</body>
</html>