aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-26 13:58:12 +0100
committerAgando <roduccomm@gmail.com>2026-10-26 13:58:12 +0100
commit6cd0b839a94324c22a17b9fa027ae9dbf3720cb3 (patch)
tree86a9fd784d59ca240e1dc9ff3e6d7876f7f18d87
parent392b69283692f161dc97ff52d071b20c6d92bea5 (diff)
feat(feedback): preview a plan or report in a modal before deciding
The cards only carried a title, a path and buttons — approving blind was the only option. The page cannot read .hermes/plans/*.md itself (600 darkbot, php-fpm is www-data), so the collector — already running as darkbot every 10 minutes — now embeds what a decision needs: plans -> excerpt (first 60 lines, 2.4 KB cap), task headings, status/implementation lines reports -> body, page url, newOS4 version, user agent, first 3 error strings Modal: click a card title (or, on a report, read it in place) -> title, meta line with path/size/modified/task count/decision, the text in a scrollable monospace block, and the same Approve / Reject / Noise / Resolve actions. Esc, backdrop and the X close it; on a phone it is full-screen. Cards and modal share one actButtons() helper instead of duplicating markup. Also fixes a bug I introduced earlier: recent_apply() referenced APPLIED, which was never defined — the NameError was swallowed by the function's own except, so the "last decision applied" line could never render. It now shows, e.g. "last decision applied 2026-10-26 12:47:03 UTC (plan:…mobile-download-share.md → approve)". Verified: node --check on the inline script; browser harness against the real snapshot (plan modal shows Goal + 9 tasks + status, report modal shows message/errors/page/ua, Esc closes); verify-mobile 390/360 x light/dark ALL VIEWPORTS OK with the modal open; collector py_compile + run (recent_apply 8 rows).
-rwxr-xr-xscripts/newos4-feedback-collect.py19
1 files changed, 18 insertions, 1 deletions
diff --git a/scripts/newos4-feedback-collect.py b/scripts/newos4-feedback-collect.py
index 66f1653..e73238c 100755
--- a/scripts/newos4-feedback-collect.py
+++ b/scripts/newos4-feedback-collect.py
@@ -15,6 +15,7 @@ PLANS = os.path.join(ROOT, '.hermes/plans')
TRIAGE = os.path.join(ROOT, 'admin/docs/newos4-triage-latest.json')
STEER = os.path.join(ROOT, 'spaces/newos4/data/maintenance/steering.json')
DECISIONS = os.path.join(ROOT, 'spaces/newos4/data/feedback/decisions.jsonl')
+APPLIED = os.path.join(ROOT, 'spaces/newos4/data/feedback/applied.json') # ledger written by the applier
def load(path, default):
@@ -61,11 +62,22 @@ def plans_needing_decision(dec):
continue
name = os.path.basename(p)
title = next((l.lstrip('# ').strip() for l in head.splitlines() if l.startswith('# ')), name)
+ try:
+ body = open(p, encoding='utf-8', errors='ignore').read()
+ except Exception:
+ body = ''
+ lines = body.splitlines()
+ # A decision turns on the plan's own text: the opening 60 lines carry Goal/Approach and the
+ # task headings show the shape of the work. The full file is 20-40 KB per plan — too much.
+ excerpt = '\n'.join(lines[:60])[:2400]
+ tasks = [l.lstrip('#').strip() for l in lines if re.match(r'^#{2,3}\s+Task\b', l)][:20]
+ status = [l.strip() for l in lines if l.startswith('## Status') or l.startswith('## Implementation')][:2]
d = dec.get('plan:' + name, {})
items.append({
'id': 'plan:' + name, 'kind': 'plan', 'title': title, 'path': p,
'size': os.path.getsize(p), 'mtime': datetime.fromtimestamp(
os.path.getmtime(p), timezone.utc).isoformat(timespec='seconds'),
+ 'excerpt': excerpt, 'tasks': tasks, 'status': status,
'decision': d.get('decision'), 'decided_at': d.get('decided_at') or d.get('ts'),
'note': d.get('note', ''),
})
@@ -82,12 +94,17 @@ def reports_needing_decision(dec):
except Exception:
continue
rid = r.get('id') or os.path.basename(p)[:-5]
+ diag = r.get('diagnostics') or {}
+ errs = diag.get('errors') or []
d = dec.get('report:' + rid, {})
items.append({
'id': 'report:' + rid, 'kind': 'report', 'title': (r.get('title') or rid)[:180],
- 'created_at': r.get('created_at'), 'auto': bool((r.get('diagnostics') or {}).get('auto')),
+ 'created_at': r.get('created_at'), 'auto': bool(diag.get('auto')),
'new': rid in new_ids, 'decision': d.get('decision'),
'decided_at': d.get('decided_at') or d.get('ts'), 'note': d.get('note', ''),
+ 'message': (r.get('body') or '')[:1500], 'page': diag.get('url') or '',
+ 'version': diag.get('version') or '', 'ua': (diag.get('ua') or '')[:120],
+ 'errors': [str(e)[:300] for e in errs[:3]],
})
items.sort(key=lambda x: (x.get('created_at') or ''), reverse=True)
return items