Skip to content

Commit 3dbdf0a

Browse files
committed
OC4.1: source-labeled historical approval summaries, verified handoffs and last activity timeline
1 parent 6f7329e commit 3dbdf0a

1 file changed

Lines changed: 85 additions & 13 deletions

File tree

‎src/lib/bob/bob_inactivity.py‎

Lines changed: 85 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import sqlite3
1414

1515
from bob_overview import snapshot as overview_snapshot
16-
from bob_publish_common import DB
16+
from bob_publish_common import DB, manual_recovery_allowed
1717

1818
SOURCE_LANES=("publisher","scout","original","dependencies")
1919
RUN=re.compile(r"\d{8}T\d{6}Z\Z")
@@ -37,6 +37,8 @@
3737
MAX_JOBS=150
3838
MAX_EVENTS=80
3939
MAX_BLOCKS=3
40+
MAX_HANDOFF_CHECKS=25
41+
MAX_LINKS=3
4042
CSS="""<style>
4143
.bi{max-width:1090px;margin:auto;display:grid;gap:12px}
4244
.bi-panel{background:linear-gradient(145deg,#162b43,#0e1b2e);border:1px solid #365575;
@@ -78,7 +80,7 @@ def _date(s):
7880

7981
def _source(when):
8082
stamp=_date(when)
81-
return stamp.isoformat(timespec="seconds") if stamp else "Time unavailable"
83+
return stamp.isoformat(timespec="seconds") if stamp else "Policy / historical summary"
8284

8385
def _reason(code,title,detail,source="",href="/logs"):
8486
return dict(code=code,title=title,detail=detail,source=_source(source),href=href)
@@ -89,7 +91,10 @@ def _blank():
8991
"reviewed":{"approved":0,"rejected":0,"unknown":0},
9092
"queue":{"manual":0,"auto":0},
9193
"approved_blocked":0,"rejected_blocked":0,"undecided_blocked":0,
92-
"sent_or_uncertain":0,"oldest_manual":"","oldest_auto":""}
94+
"sent_or_uncertain":0,"oldest_manual":"","oldest_auto":"",
95+
"job_window":{"oldest":"","newest":"","inspected":0},
96+
"latest_review_block":{"approved":"","rejected":"","unknown":""},
97+
"manual_handoff":{"checked":0,"verified":0,"run_ids":[]}}
9398

9499
def read_private(path=DB,*,at=None):
95100
"""Read-only bounded evidence. Never select arbitrary error or draft text."""
@@ -111,11 +116,20 @@ def read_private(path=DB,*,at=None):
111116
"FROM publish_jobs j LEFT JOIN decisions d ON d.run_id=j.run_id "
112117
"ORDER BY j.updated_at DESC LIMIT ?",(MAX_JOBS,)).fetchall()
113118
out["available"]=True
119+
out["job_window"]["inspected"]=len(rows)
114120
for (rid,mode,state,error,created,updated,attempted,comment,digest,
115121
challenge_code,challenge_text,challenge_expiry,verify_answer,
116122
decision,decision_hash) in rows:
117123
if mode not in ("auto","manual") or not isinstance(state,str):
118124
continue
125+
updated_time=_date(updated)
126+
if updated_time:
127+
period=out["job_window"]
128+
for field,earliest in (("oldest",True),("newest",False)):
129+
previous=_date(period[field])
130+
if previous is None or (updated_time<previous if earliest
131+
else updated_time>previous):
132+
period[field]=updated_time.isoformat(timespec="seconds")
119133
if state=="queued":
120134
out["queue"][mode]+=1
121135
key="oldest_"+mode
@@ -141,12 +155,31 @@ def read_private(path=DB,*,at=None):
141155
attempted,comment,challenge_code,challenge_text,
142156
challenge_expiry,verify_answer))
143157
if mode=="auto" and state=="failed" and error in REVIEW_BLOCK and untouched:
144-
if decision=="approved" and decision_hash==digest:
158+
classification=("approved" if decision=="approved" and decision_hash==digest
159+
else "rejected" if decision=="rejected" and decision_hash==digest
160+
else "unknown")
161+
if classification=="approved":
145162
out["approved_blocked"]+=1
146-
elif decision=="rejected" and decision_hash==digest:
163+
# No unchecked assumption that a historic DB row is
164+
# still source-verified or actionable. The same vetted
165+
# helper backs the existing owner Publish button.
166+
handoff=out["manual_handoff"]
167+
if handoff["checked"]<MAX_HANDOFF_CHECKS and RUN.fullmatch(str(rid)):
168+
handoff["checked"]+=1
169+
try:
170+
if manual_recovery_allowed(db,rid):
171+
handoff["verified"]+=1
172+
if len(handoff["run_ids"])<MAX_LINKS:
173+
handoff["run_ids"].append(rid)
174+
except (sqlite3.Error,OSError,TypeError,ValueError):
175+
pass
176+
elif classification=="rejected":
147177
out["rejected_blocked"]+=1
148178
else:
149179
out["undecided_blocked"]+=1
180+
prior=_date(out["latest_review_block"][classification])
181+
if updated_time and (prior is None or updated_time>prior):
182+
out["latest_review_block"][classification]=updated_time.isoformat(timespec="seconds")
150183
if mode=="auto" and state=="queued" and decision in ("approved","rejected") and decision_hash==digest:
151184
out["reviewed"][decision]+=1
152185
events=db.execute(
@@ -238,17 +271,25 @@ def section(key,title,lane,summary,items,href):
238271
no=history.get("rejected_blocked",0)
239272
unknown=history.get("undecided_blocked",0)
240273
if yes:
241-
reasons.append(_reason("AUTO_APPROVED_HANDOFF",f"{yes} approved auto draft(s) blocked",
242-
"Automatic sending stopped before a recorded attempt. If the verified draft is still eligible, explicitly queue it manually; approval alone does not send.",
243-
"", "/scout"))
274+
handoff=history.get("manual_handoff") or {}
275+
checked=handoff.get("checked",0)
276+
verified=handoff.get("verified",0)
277+
reasons.append(_reason("AUTO_APPROVED_HANDOFF",
278+
f"{yes} historical approved auto job(s) blocked",
279+
f"{verified} of {checked} inspected against current saved sources "
280+
"have a verified owner-controlled handoff; historical approval "
281+
"alone is not a new publication authorization. Check Scout "
282+
"for the explicit manual queue action; age, rate and duplicate "
283+
"checks still apply.",
284+
history.get("latest_review_block",{}).get("approved",""),"/scout"))
244285
if no:
245286
reasons.append(_reason("AUTO_REJECTED",f"{no} rejected auto draft(s) blocked",
246287
"Your rejection was respected. These are not awaiting approval and must not publish.",
247-
"", "/scout"))
288+
history.get("latest_review_block",{}).get("rejected",""), "/scout"))
248289
if unknown:
249290
reasons.append(_reason("REVIEW_UNCONFIRMED",f"{unknown} reviewed job(s) need checking",
250291
"The saved job reports a review block, but the stored decision is missing or does not match the draft.",
251-
"", "/scout"))
292+
history.get("latest_review_block",{}).get("unknown",""), "/scout"))
252293
if history["reviewed"].get("approved"):
253294
reasons.append(_reason("QUEUED_AUTO_APPROVED","Approved auto queue needs manual review",
254295
"An automatic queued job has an approval; automatic posting must not bypass it.",
@@ -260,7 +301,13 @@ def section(key,title,lane,summary,items,href):
260301
if not reasons:
261302
reasons.append(_reason("AUTO_UNKNOWN","Automatic activity not established",
262303
"Check Scout selections, review decisions and publisher events.","","/scout"))
263-
auto_summary=("Auto queue: "+str(auto) if auto is not None else "Auto queue not reported")
304+
window=history.get("job_window") or {}
305+
span=(" · history "+str(window.get("oldest"))[:10]+" to "+
306+
str(window.get("newest"))[:10]+" UTC" if window.get("oldest")
307+
and window.get("newest") else " · history range unavailable")
308+
auto_summary=(("Auto queue: "+str(auto) if auto is not None
309+
else "Auto queue not reported")+
310+
span+" · up to "+str(MAX_JOBS)+" recent jobs inspected")
264311
lines.append(section("auto","Automatic comments",
265312
publisher,auto_summary,reasons,"/automation"))
266313
# Original posting is explicitly owner requested.
@@ -307,6 +354,11 @@ def section(key,title,lane,summary,items,href):
307354
"last_skip":history.get("last_skip"),
308355
"policy_next":(metrics.get("next_possible_utc") or {}).get("comments") or "",
309356
"scheduled_next":publisher.get("next_scheduled") or "",
357+
"scout_checkpoint":scout.get("updated_at") or "",
358+
"scout_activity":scout.get("activity") or "Not confirmed",
359+
"handoff_links":(history.get("manual_handoff") or {}).get("run_ids",[])[:MAX_LINKS],
360+
"handoff_verified":(history.get("manual_handoff") or {}).get("verified",0),
361+
"publisher_last_skip":history.get("last_skip"),
310362
"notice":"Reason codes are deterministic local observations, not a model's guess. "
311363
"A saved approval does not mean a comment was sent."}
312364

@@ -324,13 +376,22 @@ def page(wrap,*,data=None):
324376
'<a href="/investigate">Refresh evidence</a></div>',
325377
'<p class="bi-muted">Checked '+e(item["checked_at"])+
326378
' · no actions or retries are performed here.</p></section>',
327-
'<section class="bi-panel"><h2>Publication timing</h2>',
379+
'<section class="bi-panel"><h2>Last activity &amp; timing</h2>',
380+
'<p class="bi-line">Most recent Scout checkpoint: <b>'+
381+
e(item.get("scout_checkpoint") or "Not recorded")+'</b> · '+
382+
e(item.get("scout_activity") or "Status unconfirmed")+'</p>',
383+
'<p class="bi-line">Last publisher evaluation: <b>'+
384+
e((item.get("publisher_last_skip") or {}).get("at") or
385+
"No recent safe skip record")+'</b> · '+
386+
e((item.get("publisher_last_skip") or {}).get("code") or
387+
"No recorded skip")+'</p>',
328388
'<p class="bi-line">Last recorded attempt: <b>'+
329389
e(item["last_attempt"] or "Not recorded")+'</b></p>',
330390
'<p class="bi-line">Last API-acknowledged publication: <b>'+
331391
e(item["last_success"] or "Not recorded in recent inspected jobs")+'</b></p>',
332392
'<p class="bi-line">Next systemd publisher trigger: <b>'+
333-
e(item["scheduled_next"] or "Not reported")+'</b></p>',
393+
e(item["scheduled_next"] or
394+
"Not reported by systemd; check Automation / timer state")+'</b></p>',
334395
'<p class="bi-line">Earliest policy-permitted comment: <b>'+
335396
e(item["policy_next"] or "Not reported")+'</b> (not scheduled)</p>',
336397
'<p class="bi-muted">API acknowledgement is not independent proof that '
@@ -350,5 +411,16 @@ def page(wrap,*,data=None):
350411
' · '+e(reason["source"])+'</span></div>']
351412
parts += ['</div><p class="bi-line"><a href="'+e(section["href"])+
352413
'">Inspect supporting records</a></p></section>']
414+
if item.get("handoff_links"):
415+
parts += ['<section class="bi-panel"><h2>Source-verified manual handoffs</h2>',
416+
'<p class="bi-muted">These historical jobs still pass the '
417+
'read-only saved-source check. Links open owner review; '
418+
'nothing is sent or queued from this page. '
419+
'The publisher will still enforce age, rate and duplicate safety checks.</p>',
420+
'<div class="bi-links">']
421+
for rid in item["handoff_links"][:MAX_LINKS]:
422+
if RUN.fullmatch(str(rid)):
423+
parts.append('<a href="/scout#run-'+e(rid)+'">Review '+e(rid)+'</a>')
424+
parts.append('</div></section>')
353425
parts += ['</div></div>']
354426
return wrap("Why no activity?","".join(parts),"/investigate")

0 commit comments

Comments
 (0)