T11.6: retire the per-browser Usage report (CR-019)

Removes the old "Usage logs" card from admin.html/admin.js (the
usage-admin panel + loadUsage(), D5/T7.10) now that T11.5's real,
server-side Activity & usage card exists and reads real per-user data
instead of per-browser localStorage.

wp-usage.js is deleted outright, along with its three <script> includes
(admin.html, wp-creation-index.html, work-package-suite.html) - it had
no reader left once the panel above it was removed (the "download the
full event log" button lived only in that panel), and per the original
CR-019 decision record it was never reliably tied to a real identity,
so it was never a candidate data source for the new report either.

Its two call sites (wp-creation-app.js, work-package-suite-app.js) keep
a local track() function as a documented no-op rather than having each
of their ~45 individual track('event', ...) call sites deleted one at a
time - that would be a much larger, riskier diff for the same outcome
(no data is recorded either way), and each call site still marks what
was worth recording if usage analytics are ever rebuilt server-side.
work-package-suite-app.js's dwell-timer plumbing (_stepEnter /
trackStepDwell), which only ever fed track(), was left in place for the
same reason: inert, not broken.

Also removes tests/usage_check.py, which tested exactly the retired
feature, and updates its line in docs/reference/file-map.md to point at
the 2026-09-17 decision record instead.

Verified:
  - grep across the whole repo for WPUsage / wp-usage.js / usage-admin /
    usage_check: no live references remain, only explanatory comments
    and planning docs (decisions-2026-09-17.md, wave-11.md) that
    describe the removal itself
  - node --check on all three touched .js files: no syntax errors
  - full backend smoke test (27/27) and seed_demo.py still pass
  - tests/baseline_shots.py --pages admin,creator,sop at 390px/1440px,
    run locally: all three pages render with no new JS errors (the one
    "beforeunload" log line on sop/creator at 1440px is pre-existing
    harness noise from wp-autosave.js's unsaved-work guard, unrelated
    to this change) and no horizontal overflow; refreshed baseline
    screenshots committed alongside this change
This commit is contained in:
2026-09-23 14:46:32 -07:00
parent 0b5ab59518
commit b2a083ac7c
16 changed files with 55 additions and 333 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -283,7 +283,9 @@ python tests/triage_check.py # A6 - the sidebar answers the stand-up
python tests/qa_gate_check.py # CR-014/D2/D9/D10 - QA gate + capture sink 41 checks python tests/qa_gate_check.py # CR-014/D2/D9/D10 - QA gate + capture sink 41 checks
python tests/files_check.py # CR-007/D8 - drawings upload + real offline 36 checks python tests/files_check.py # CR-007/D8 - drawings upload + real offline 36 checks
python tests/sticky_bar_check.py # B6 - save reachable on every wizard step 12 checks python tests/sticky_bar_check.py # B6 - save reachable on every wizard step 12 checks
python tests/usage_check.py # D5 - one analytics core, admin report 15 checks # tests/usage_check.py (D5) removed at T11.6 - it tested the per-browser
# analytics core and admin report, both retired in favour of CR-019's
# server-side Activity & usage card (see docs/waves/decisions-2026-09-17.md).
python tests/creator_dialogs_check.py # S1 creator - 0 natives, errors at fields 20 checks python tests/creator_dialogs_check.py # S1 creator - 0 natives, errors at fields 20 checks
``` ```

View File

@@ -192,13 +192,37 @@ records before deciding — do not assume from this file alone).
**Do not:** leave the old panel in place "just in case." Two activity reports **Do not:** leave the old panel in place "just in case." Two activity reports
showing two different numbers is worse than one. showing two different numbers is worse than one.
**Decision (2026-09-23):** `wp-usage.js` is removed, not kept — it had no
reader left once the admin panel above it was removed (the "download the
full event log" button lived only in that panel), and per the original
decision record it was never reliably tied to a real identity, so it was
never a candidate source for the new report either. The file itself and its
three `<script>` includes (`admin.html`, `wp-creation-index.html`,
`work-package-suite.html`) are gone. Its two call sites
(`wp-creation-app.js`, `work-package-suite-app.js`) keep a local `track()`
function as a documented no-op rather than having each of their ~45
individual `track('event', …)` call sites deleted one at a time — that
would be a far larger, riskier diff for the same outcome (no more data is
recorded either way), and it keeps each call site as a marker of what was
worth recording if usage analytics are ever rebuilt server-side. The
dwell-timer plumbing that only ever fed `track()` (`work-package-suite-app.js`'s
`_stepEnter`/`trackStepDwell`) was left in place for the same reason — it is
inert now, not broken, and touching it buys nothing.
Also removed: `tests/usage_check.py` (tested exactly the retired feature —
D5/T7.10's per-browser analytics core and admin report) and its line in
`docs/reference/file-map.md`, replaced with a note pointing at this decision.
**Done when:** **Done when:**
- [ ] the old `usage-admin` panel and its markup are gone from `admin.html`/ - [x] the old `usage-admin` panel and its markup are gone from `admin.html`/
`admin.js` `admin.js`
- [ ] a decision on `wp-usage.js` itself is recorded (removed, or kept with a - [x] a decision on `wp-usage.js` itself is recorded (removed, or kept with a
stated reason) — not left ambiguous stated reason) — not left ambiguous — see above
- [ ] nothing else in the app references the removed code; grep confirms - [x] nothing else in the app references the removed code; grep confirms
(only remaining hits are this file, the 2026-09-17 decision record, and
the two explanatory code comments left at the retired call sites — all
prose, not live references)
--- ---

View File

@@ -34,12 +34,12 @@
/* Every container admin.js paints a table into is a scrollport of its own, so a /* 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 sticky header always has something to stick to rather than sliding up behind
the app bar. Same rule as console.css's .tscroll. */ the app bar. Same rule as console.css's .tscroll. */
#comments-admin, #audit-admin, #notif-box, #usage-admin, #activity-admin, #projects-table, #defmem-table{ #comments-admin, #audit-admin, #notif-box, #activity-admin, #projects-table, #defmem-table{
overflow:auto; max-height:min(70vh,640px); overscroll-behavior:contain; } 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 /* If admin.js wraps its table in its own .tscroll, the outer box steps aside so
one table never ends up with two scrollbars. */ one table never ends up with two scrollbars. */
#comments-admin:has(.tscroll), #audit-admin:has(.tscroll), #notif-box:has(.tscroll), #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){ #activity-admin:has(.tscroll), #projects-table:has(.tscroll), #defmem-table:has(.tscroll){
overflow:visible; max-height:none; } overflow:visible; max-height:none; }
/* Comment text and audit detail are the two columns you are actually here to /* Comment text and audit detail are the two columns you are actually here to
@@ -53,7 +53,7 @@
border:1px solid var(--border-strong); border-radius:0; margin-bottom:var(--s3); } border:1px solid var(--border-strong); border-radius:0; margin-bottom:var(--s3); }
@media (max-width:900px){ @media (max-width:900px){
#comments-admin, #audit-admin, #notif-box, #usage-admin, #activity-admin, #projects-table, #defmem-table{ #comments-admin, #audit-admin, #notif-box, #activity-admin, #projects-table, #defmem-table{
max-height:none; } max-height:none; }
} }
</style> </style>
@@ -209,18 +209,6 @@
<div id="activity-export-banner" class="banner" style="display:none"></div> <div id="activity-export-banner" class="banner" style="display:none"></div>
</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 --> <!-- DB SNAPSHOT -->
<div class="card"> <div class="card">
<h2>Database snapshot</h2> <h2>Database snapshot</h2>
@@ -249,7 +237,6 @@
</div> </div>
</div> </div>
<script src="wp-usage.js"></script>
<script src="console-util.js"></script> <script src="console-util.js"></script>
<script src="wp-dialog.js"></script> <script src="wp-dialog.js"></script>
<script src="admin.js"></script> <script src="admin.js"></script>

View File

@@ -25,7 +25,6 @@ function reveal(){
loadComments(); loadComments();
loadAudit(); loadAudit();
loadActivity(); loadActivity();
loadUsage();
} }
function showDenied(){ function showDenied(){
document.getElementById('admin-denied').style.display=''; document.getElementById('admin-denied').style.display='';
@@ -754,48 +753,6 @@ async function loadNotifications(){
'</tr>').join('')+'</tbody></table>'; '</tr>').join('')+'</tbody></table>';
} }
// ── usage logs (read from this browser's localStorage) ──────────────────────────
// D5 / T7.10: the report for BOTH tools' recorded usage, in the one place an
// operator-facing readout belongs - behind the same admin gate as this whole
// page (gateByRole() below shows nothing else either). Data comes from
// wp-usage.js, the single implementation; the keys predate the move, so
// everything recorded before it is still here.
function loadUsage(){
const box = document.getElementById('usage-admin');
if(!box) return;
const tools = [
['Work package creator', WPUsage.KEYS.creator, 'wp-iwp-usage'],
['SOP wizard', WPUsage.KEYS.wizard, 'wp-suite-usage'],
];
let html = '';
tools.forEach(([label, key, prefix]) => {
const evs = (WPUsage.load(key).events) || [];
html += '<h2 style="margin-top:16px">' + uesc(label) + '</h2>';
if(!evs.length){
html += '<div class="note">No usage recorded in this browser yet.</div>';
return;
}
const byEvent = {}, sessions = new Set();
let first = evs[0].ts, last = evs[0].ts;
evs.forEach(e => {
byEvent[e.event] = (byEvent[e.event]||0)+1;
if(e.session) sessions.add(e.session);
if(e.ts < first) first = e.ts; if(e.ts > last) last = e.ts;
});
const fmt = v => v ? wpFormatDateTime(v) : '—';
html += '<table class="kv">'+
'<tr><th>Sessions</th><td>'+sessions.size+'</td></tr>'+
'<tr><th>Events</th><td>'+evs.length+'</td></tr>'+
'<tr><th>Range</th><td style="font-weight:600">'+fmt(first)+' → '+fmt(last)+'</td></tr></table>';
html += '<table class="users"><thead><tr><th>Event</th><th>Count</th></tr></thead><tbody>';
Object.keys(byEvent).sort().forEach(k => html += '<tr><td>'+uesc(k)+'</td><td>'+byEvent[k]+'</td></tr>');
html += '</tbody></table>';
html += '<div class="toolbar" style="margin-top:8px"><button onclick="WPUsage.download(WPUsage.KEYS.'+
(key === WPUsage.KEYS.creator ? 'creator' : 'wizard')+', ' + jsq(prefix) + ')">Download the full event log</button></div>';
});
box.innerHTML = html;
}
// ── access control: admins only ───────────────────────────────────────────────── // ── access control: admins only ─────────────────────────────────────────────────
// auth-guard.js requires a login and sets window.WP_USER (firing 'wp-auth-ready'). // auth-guard.js requires a login and sets window.WP_USER (firing 'wp-auth-ready').
// Show the console for admins; otherwise show the "Admins only" notice. // Show the console for admins; otherwise show the "Admins only" notice.

View File

@@ -2207,20 +2207,18 @@ function loadStepComments(){
} }
} }
// ── USAGE ANALYTICS ───────────────────────────────────────────────────────── // ── USAGE ANALYTICS (retired, T11.6) ────────────────────────────────────────
// Lightweight usage analytics stored in localStorage so the tool owner can review // This used to write to wp-usage.js's per-browser localStorage log (D5/T7.10),
// engagement over time. No field VALUES are stored (field-edit events record only // read back by the admin console's old "Usage logs" panel. CR-019 replaced
// the field id), keeping captured data non-sensitive. // both with real, server-side, per-user activity (UsageEvent + the Activity &
// D5 / T7.10: the analytics implementation lives in wp-usage.js and the report // usage card) - see decisions-2026-09-17.md for why this per-browser data was
// on the admin console. The wizard's own copy of showAnalytics() never had a // never a source the new report could adopt. track() is now a no-op; kept
// caller here - the button lived on the creator - and once B7 dissolved the // (rather than deleting its handful of call sites, including the dwell-timer
// frame the duplicate sat in the same document as five colliding globals. // plumbing below) so this stays a one-line change instead of touching every
// This page only records; dwell tracking keeps its page-local state below. // caller for the same outcome.
let _stepEnter = Date.now(); let _stepEnter = Date.now();
function track(event, detail){ function track(event, detail){ /* retired, T11.6 — see comment above */ }
WPUsage.track(WPUsage.KEYS.wizard, event, detail);
}
function trackStepDwell(){ function trackStepDwell(){
const ms = Date.now() - _stepEnter; const ms = Date.now() - _stepEnter;
if(ms > 400 && ms < 1000*60*60) track('step_dwell', {step: currentStep, ms}); if(ms > 400 && ms < 1000*60*60) track('step_dwell', {step: currentStep, ms});

View File

@@ -11,7 +11,6 @@
<!-- Addressable state (S3). Parses before the app scripts, which read the URL <!-- Addressable state (S3). Parses before the app scripts, which read the URL
during their own boot. --> during their own boot. -->
<script src="wp-url.js"></script> <script src="wp-url.js"></script>
<script src="wp-usage.js"></script>
<script src="wp-list-import.js"></script> <script src="wp-list-import.js"></script>
<!-- Autosave, unsaved-work guard, draft recovery (S2). --> <!-- Autosave, unsaved-work guard, draft recovery (S2). -->
<script src="wp-autosave.js"></script> <script src="wp-autosave.js"></script>

View File

@@ -3993,11 +3993,17 @@ function openSopModal(){
document.getElementById('sop-modal').classList.add('open'); track('view_sop'); document.getElementById('sop-modal').classList.add('open'); track('view_sop');
} }
function closeSopModal(){ document.getElementById('sop-modal').classList.remove('open'); } function closeSopModal(){ document.getElementById('sop-modal').classList.remove('open'); }
// D5 / T7.10: the analytics implementation lives in wp-usage.js - ONE copy for // CR-019 / T11.6 (2026-09-23): this used to write to wp-usage.js's per-browser
// the whole suite - and its report lives on the admin console, where an // localStorage log (D5/T7.10). Retired along with the admin console's old
// operator-facing readout belongs. This page only records. Same key, same // "Usage logs" panel, its only reader - real, server-side, per-user activity
// event shape: everything recorded before the move is still readable after it. // now exists (UsageEvent, the Activity & usage card, T11.1-T11.5) and this
function track(event,detail){ if(devMode) return; WPUsage.track(WPUsage.KEYS.creator, event, detail); } // data was never reliably tied to a real identity anyway, so it was not a
// source the new report could adopt (decisions-2026-09-17.md). track() stays
// as a no-op rather than deleting its ~35 call sites throughout this file:
// removing every call individually is a much larger, riskier diff for the
// same outcome, and a call site here still documents the moment worth
// recording if usage analytics are ever rebuilt server-side.
function track(event,detail){ /* retired, T11.6 — see comment above */ }
// ── COMMENTS ───────────────────────────────────────────────────────────────── // ── COMMENTS ─────────────────────────────────────────────────────────────────
const COMMENTS_KEY='wp_iwp_comments_v1'; const COMMENTS_KEY='wp_iwp_comments_v1';

View File

@@ -11,7 +11,6 @@
<!-- Addressable state (S3). Parses before the app scripts, which read the URL <!-- Addressable state (S3). Parses before the app scripts, which read the URL
during their own boot. --> during their own boot. -->
<script src="wp-url.js"></script> <script src="wp-url.js"></script>
<script src="wp-usage.js"></script>
<!-- Autosave, unsaved-work guard, draft recovery (S2). --> <!-- Autosave, unsaved-work guard, draft recovery (S2). -->
<script src="wp-autosave.js"></script> <script src="wp-autosave.js"></script>
<!-- Which sections this project uses (CR-006). The same file the SOP wizard <!-- Which sections this project uses (CR-006). The same file the SOP wizard

View File

@@ -1,58 +0,0 @@
/* Usage analytics core — the ONE implementation (D5 / T7.10).
This existed three times: the creator's copy, the wizard's copy (which had no
caller — the button lived on the creator), and the admin console's own reader.
Once the creator stopped being an iframe (B7/T7.1) the first two sat in one
document as five colliding globals; an unreferenced duplicate is exactly what
produced D5. One core now; the pages keep only a thin track() wrapper because
page state (the creator's dev-mode pause) belongs to the page.
The storage KEYS are unchanged on purpose: everything recorded before this
file existed is still readable through it. No field VALUES are ever stored —
a field-edit event records the field id, nothing else.
Classic script, no modules: exposes window.WPUsage. */
'use strict';
(function () {
var SESSION = 's_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
function load(key) {
try { return JSON.parse(localStorage.getItem(key)) || { events: [] }; }
catch (e) { return { events: [] }; }
}
function save(key, data) {
try { localStorage.setItem(key, JSON.stringify(data)); }
catch (e) { /* storage unavailable — degrade silently */ }
}
function track(key, event, detail) {
try {
var d = load(key);
d.events.push({ ts: new Date().toISOString(), session: SESSION, event: event, detail: detail || null });
if (d.events.length > 5000) d.events = d.events.slice(-5000);
save(key, d);
} catch (e) { /* never let telemetry break the tool it watches */ }
}
function download(key, prefix) {
var blob = new Blob([JSON.stringify(load(key), null, 2)], { type: 'application/json' });
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = (prefix || 'wp-usage') + '-' + new Date().toISOString().slice(0, 10) + '.json';
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(function () { URL.revokeObjectURL(a.href); }, 1000);
}
window.WPUsage = {
load: load,
save: save,
track: track,
download: download,
// The pre-D5 keys, verbatim — continuity of the recorded data is a done-when.
KEYS: { creator: 'wp_iwp_analytics_v1', wizard: 'wp_suite_analytics_v1' },
};
})();

View File

@@ -1,192 +0,0 @@
#!/usr/bin/env python3
"""Is there exactly one analytics implementation, reported from admin? — D5, T7.10.
Usage analytics existed twice (creator + wizard), five of the nine colliding
globals `creator-frame.md` counted, and the wizard's copy had no caller. One
core survives in wp-usage.js; the pages keep thin track() wrappers; the report
and its downloads live on the admin console behind the same role gate as the
rest of that page. Keys are unchanged, so pre-move data still reads.
Self-contained: throwaway SQLite, its own uvicorn, headless Edge or Chrome.
Exit 0 all passed, 1 a failure, 2 could not run.
"""
import json
import os
import re
import sys
import tempfile
import time
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import cdp # noqa: E402
from browser_check import seed, start_server, chk, _PASS, _FAIL # noqa: E402
from sections_check import set_sop # noqa: E402
from stepper_check import dismiss_dialogs # noqa: E402
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
HTML = os.path.join(ROOT, "html")
def ascii_(v, n=280):
return re.sub(r"\s+", " ", str(v)).encode("ascii", "replace").decode()[:n]
def settle(seconds=0.6):
time.sleep(seconds)
def strip_js(src):
src = re.sub(r"/\*.*?\*/", "", src, flags=re.S)
return "\n".join(re.sub(r"(?<![:'\"])//.*$", "", ln) for ln in src.split("\n"))
def main():
exe = cdp.find_browser()
if not exe:
print("no headless-capable browser found; set WP_BROWSER.")
return 2
# ── 1. the grep half: one implementation, no leftover controls ───────────
print("\n1. grep: one implementation, nothing unreferenced")
files = {}
for name in os.listdir(HTML):
if name.endswith((".js", ".html")):
files[name] = strip_js(open(os.path.join(HTML, name), encoding="utf-8").read())
core_defs = [n for n, src in files.items() if "window.WPUsage" in src]
chk("the WPUsage core is defined in wp-usage.js and only there",
core_defs == ["wp-usage.js"], ascii_(core_defs))
chk("the pages record THROUGH it - no page touches the storage keys directly",
all("wp_iwp_analytics_v1" not in src and "wp_suite_analytics_v1" not in src
for n, src in files.items()
if n not in ("wp-usage.js",) and n.endswith(".js")))
leftovers = {n: re.findall(r"analyticsLoad|analyticsSave|downloadAnalytics|showAnalytics"
r"|ANALYTICS_KEY|USAGE_KEY|usageLoad|downloadUsage", src)
for n, src in files.items() if n != "wp-usage.js"}
leftovers = {n: v for n, v in leftovers.items() if v}
chk("none of the five colliding globals survives anywhere; grep confirms",
not leftovers, ascii_(leftovers))
chk("no analytics control remains on the creator or the wizard; grep confirms",
"Usage data" not in files["wp-creation-index.html"]
and "showAnalytics" not in files["work-package-suite.html"])
chk("both storage keys survive, verbatim, in the core (data continuity)",
"wp_iwp_analytics_v1" in files["wp-usage.js"]
and "wp_suite_analytics_v1" in files["wp-usage.js"])
tmpdir = tempfile.mkdtemp(prefix="wpsuite-usage-")
db_path = os.path.join(tmpdir, "check.db")
server = None
browser = None
try:
tok = seed(db_path)
set_sop(db_path, {})
port = cdp.free_port()
base = "http://127.0.0.1:%d" % port
server = start_server(port, db_path)
browser = cdp.Browser(exe)
page = browser.page()
page.clear_cookies()
page.set_cookie("wp_session", tok["root"])
page.viewport(1440, 900)
# ── 2. recording still works from both tools ─────────────────────────
print("\n2. the tools still record")
page.goto(base + "/wp-creation-index.html?project=projA")
dismiss_dialogs(page)
settle(2.0)
# Plant a LEGACY-format event under the pre-move key: the done-when is
# that data recorded before this task is still readable after it.
page.eval("""(() => {
const d = JSON.parse(localStorage.getItem('wp_iwp_analytics_v1')) || {events: []};
d.events.unshift({ts: '2026-07-01T10:00:00Z', session: 's_legacy',
event: 'legacy_probe_event', detail: null});
localStorage.setItem('wp_iwp_analytics_v1', JSON.stringify(d));
})()""")
n0 = page.eval("WPUsage.load(WPUsage.KEYS.creator).events.length")
page.eval("track('probe_event')")
chk("the creator's track() still lands events under the old key",
page.eval("WPUsage.load(WPUsage.KEYS.creator).events.length") == n0 + 1)
page.goto(base + "/work-package-suite.html?tab=sop")
dismiss_dialogs(page)
settle(2.0)
n0 = page.eval("WPUsage.load(WPUsage.KEYS.wizard).events.length")
page.eval("track('probe_event_wizard')")
chk("the wizard's track() still lands events under its old key",
page.eval("WPUsage.load(WPUsage.KEYS.wizard).events.length") == n0 + 1)
wiz_errors = [e for e in page.js_errors() if "beforeunload" not in e]
chk("...and the wizard page throws no errors without its old globals "
"(the blocked-beforeunload console line is BL-020, filtered not hidden)",
not wiz_errors, ascii_(wiz_errors[:2]))
# ── 3. the report, on admin, behind the admin gate ───────────────────
print("\n3. the admin report")
page.goto(base + "/admin.html")
dismiss_dialogs(page)
settle(2.0)
page.eval("loadUsage()")
settle(0.5)
report = page.eval("(document.getElementById('usage-admin')||{textContent:''}).textContent")
chk("usage data is reachable from admin.html, both tools reported",
"Work package creator" in report and "SOP wizard" in report, ascii_(report, 160))
chk("the pre-move legacy event is readable in the report",
"legacy_probe_event" in report)
chk("this session's fresh events are in it too",
"probe_event" in report and "probe_event_wizard" in report)
chk("each tool offers its download from the report",
page.eval("[...document.querySelectorAll('#usage-admin button')].length") >= 2)
page.viewport(390, 844, mobile=True)
settle(0.6)
fits = page.eval("""(() => {
const box = document.getElementById('usage-admin');
return box && box.scrollWidth <= box.clientWidth + 2
&& document.documentElement.scrollWidth <= 392;
})()""")
chk("the report is usable at 390px - no sideways scrolling", bool(fits))
page.viewport(1440, 900)
settle(0.4)
# The same role gate as the rest of the console: a non-admin sees the
# denied card and no cards, this one included.
page.clear_cookies()
page.set_cookie("wp_session", tok["pat"])
page.goto(base + "/admin.html")
dismiss_dialogs(page)
settle(2.0)
chk("a non-administrator gets the denied notice, not the usage report",
page.eval("""(() => {
const denied = document.getElementById('admin-denied');
const usage = document.getElementById('usage-admin');
const visible = el => !!el && el.offsetParent !== null;
return visible(denied) && !visible(usage);
})()"""))
js_errors = [e for e in page.js_errors() if "beforeunload" not in e]
chk("no JavaScript errors anywhere in this run", not js_errors,
ascii_(js_errors[:2]))
finally:
if browser is not None:
try:
browser.close()
except Exception:
pass
if server is not None:
try:
server.terminate()
except Exception:
pass
print("\n" + "-" * 54)
print("%d/%d checks passed." % (len(_PASS), len(_PASS) + len(_FAIL)))
for f in _FAIL:
print(" - " + f)
return 1 if _FAIL else 0
if __name__ == "__main__":
sys.exit(main())