From 431cef232d2804c0568ff3f3c1e95caaa6be6870 Mon Sep 17 00:00:00 2001 From: Agando Date: Mon, 2 Nov 2026 20:31:05 +0100 Subject: feat(feedback): chat plane on the dashboard + honest triage ranking Tasks 3-8 of the chat-plane plan: - cron newos4-chat-ingest (0cc4bb4d2f82, */10, script-only, silent when idle) - collect.py: plan_key() normalises the two ledger spellings on read (5 rows carried a plan name without .md and were silently unmatched); per-item bulk_pass for the 2026-10-26 pass; counts.chat_total - feedback.html: 5th tile "from chat"; card shows who closed an item and when, with a bulk marker - triage: UNFILEABLE class -> top_real now ranks actionable errors only (first row went from an 18 203 fetch failure to the 989 toLowerCase runtime error); network/abort signatures move to policy_noise - AGENTS.md: the chat plane documented as an intake Verified: autoreview --dry-run over the 5 chat reports -> "nothing provable (5 left for the operator)" (R6 refuses auto:false); maintain 16 PASS 0 FAIL; feedback.html inline JS node --check OK; collect -> awaiting 5, chat_total 5. --- scripts/newos4-triage.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) (limited to 'scripts/newos4-triage.py') diff --git a/scripts/newos4-triage.py b/scripts/newos4-triage.py index ef05dc1..651b94d 100755 --- a/scripts/newos4-triage.py +++ b/scripts/newos4-triage.py @@ -16,6 +16,15 @@ STATE_DIR = '/var/lib/webx11/newos4-maintenance' STATE = os.path.join(STATE_DIR, 'state.json') NOISE = re.compile(r'(user aborted|aborted without reason|signal timed out|AbortError|TimeoutError)', re.I) +# api/debug/report.php refuses network-class auto reports and the supervisor skips aborts by design, +# so these can never become a card. Ranking them by count taught the operator to ignore top_real: +# 18 203 / 16 540 / 13 407 / 423 sat above the only action-able runtime error (989). Measured 2026-11-02. +UNFILEABLE = re.compile(r'(failed to fetch|fetch failed|networkerror|load failed|' + r'aborted without reason|signal timed out|err_network)', re.I) + + +def is_unfileable(msg): + return bool(UNFILEABLE.search(str(msg or ''))) SELF = re.compile(r'/api/debug/(ingest|report)\.php', re.I) @@ -156,7 +165,14 @@ def build(apply_fix=False): ({'message': safe(e.get('message'), 200), 'type': e.get('type'), 'count': int(e.get('count') or 1), 'age_hours': age_hours(e.get('last_seen')), 'frame': safe(e.get('frame'), 160), - 'session': safe(e.get('session_id'), 40)} for e in buckets['real']), + 'session': safe(e.get('session_id'), 40)} for e in buckets['real'] + if not is_unfileable(e.get('message'))), + key=lambda x: -x['count'])[:10], + # Ranked separately: these can never become a card (policy), so they are not "top real". + 'policy_noise': sorted( + ({'message': safe(e.get('message'), 200), 'type': e.get('type'), + 'count': int(e.get('count') or 1), 'age_hours': age_hours(e.get('last_seen'))} + for e in buckets['real'] if is_unfileable(e.get('message'))), key=lambda x: -x['count'])[:10], 'new_reports': [{'id': r.get('id'), 'title': safe(r.get('title'), 160), 'created_at': r.get('created_at'), -- cgit v1.2.3