Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/workflows/dashboard_refresh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ name: Dashboard Refresh
# - pull_request: a dry run (ingest + render, no commit) so a reader change
# is exercised against the live feeds before it merges.
#
# Receipts and snapshots carry their `fetched_at` while nothing changes, the
# pages carry no wall-clock stamp, and state.json's `updated` moves only when
# the feed's content does, so a night with no new project commit commits
# nothing. A failed fetch keeps the committed snapshot (shown as cached, never
# Successful refreshes update receipt observation metadata and panel freshness.
# Snapshot capture/evidence dates remain unchanged on identical input; state.json
# `updated` still moves only when feed content changes. A refresh can commit
# observation metadata even when project evidence is unchanged. A failed fetch
# keeps the committed snapshot (shown as cached, never
# re-aged). The body map is read from a checkout of PyAutoMind (public). The
# bot commit uses GITHUB_TOKEN, which never triggers other workflows, so after
# a commit this workflow dispatches pages_dashboard.yml explicitly.
Expand Down
12 changes: 12 additions & 0 deletions insight/board.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,17 @@ def _detail(s, now=None):
return "".join(parts)


def _captured_at(views):
"""Oldest successful observation of displayed inputs; failed or missing inputs stay unknown.

Capture time is separate from producer evidence and content-only state.updated.
"""
if not views or any(v.outcome != "ok" or not v.refreshed_at for v in views):
return None
times = [summary.parse_utc(v.refreshed_at) for v in views]
return min(times) if all(times) else None


def render_html(views, now=None, campaign_data=None):
data = campaign_data if campaign_data is not None else campaigns.load()
shared = theme()
Expand Down Expand Up @@ -270,6 +281,7 @@ def render_html(views, now=None, campaign_data=None):
+ input_marker(views, data)
+ campaigns.render_html(
data,
refreshed_at=_captured_at(views),
work_links=[
{"label": "PyAutoInsight", "href": REPO_URL},
*({"label": v.instance.repo, "href": v.instance.github_url} for v in views),
Expand Down
11 changes: 9 additions & 2 deletions insight/campaigns.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def markdown(data: dict) -> str:
return "\n".join(rows)


def render_html(data: dict, work_links=()) -> str:
def render_html(data: dict, work_links=(), refreshed_at=None) -> str:
def e(value):
return html.escape(str(value), quote=True)

Expand Down Expand Up @@ -236,7 +236,14 @@ def table(headers, rows):
return (
marker(data)
+ theme().orchestration_panel(
"insight", "", "", PROMPT, work_links=work_links, organ="insight"
"insight",
"",
"",
PROMPT,
work_links=work_links,
organ="insight",
refreshed_at=refreshed_at,
refresh_url="https://github.com/PyAutoLabs/PyAutoInsight/actions/workflows/dashboard_refresh.yml",
)
+ f'<p class="muted">Last check-in: {e(data.get("last_checkin") or "not recorded yet")}.</p>'
'<h2 id="campaigns">Active campaigns</h2>'
Expand Down
20 changes: 16 additions & 4 deletions insight/ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
that snapshot and the board shows it labelled **cached**, with its original
evidence and fetch times and the new error: nothing re-ages it.

Receipts and snapshots are written only when their content changes (the
``fetched_at`` of an unchanged outcome at the same commit is carried), so a
nightly refresh against an unchanged project commits nothing. A local
Snapshots preserve capture history for unchanged input. Receipts separately
record ``refreshed_at`` for each successful observation, so real refreshes
can commit metadata even when evidence is unchanged. Offline replay retains
the recorded observation time; a failed attempt exposes no successful refresh.
A local
``--from`` read reports the checkout's revision and dirty state, is labelled
"local checkout", and writes no receipt or snapshot: it cannot masquerade as
a published capture.
Expand Down Expand Up @@ -110,6 +112,7 @@ class Snapshot:
dirty: bool | None = None
checkout: str | None = None
source_branch: str | None = None
refreshed_at: str | None = None # latest successful observation, independent of capture

@property
def failed(self) -> bool:
Expand Down Expand Up @@ -180,6 +183,7 @@ def _from_files(inst: Instance, out_dir: Path) -> Snapshot:
errors=[str(e) for e in receipt.get("errors") or []],
attempt_commit=receipt.get("resolved_commit"),
attempt_at=receipt.get("fetched_at"),
refreshed_at=receipt.get("refreshed_at") if receipt.get("outcome") == "ok" else None,
source_branch=receipt.get("branch"),
)
if receipt is not None:
Expand Down Expand Up @@ -296,7 +300,13 @@ def ingest(
"errors": errors,
"cached_from": last_good.get("fetched_at") if last_good and outcome != "ok" else None,
}
_carry(receipt_path(out_dir, inst.instance), receipt)
# Preserve capture history while recording a real successful observation.
prior = read_receipt(out_dir, inst.instance)
strip = lambda d: {k: v for k, v in d.items() if k not in ("fetched_at", "refreshed_at")} # noqa: E731
if prior is not None and strip(prior) == strip(receipt):
receipt["fetched_at"] = prior["fetched_at"]
receipt["refreshed_at"] = now if outcome == "ok" else None
_carry(receipt_path(out_dir, inst.instance), receipt, keys=())
return _from_files(inst, out_dir)


Expand Down Expand Up @@ -337,4 +347,6 @@ def _ingest_local(inst: Instance, path: Path, wanted) -> Snapshot:
view.outcome, view.errors = "invalid", ["project identity differs from registry"]
if view.failed:
view.doc = None
else:
view.refreshed_at = view.fetched_at
return view
17 changes: 17 additions & 0 deletions tests/test_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def view(registry_file, fake_mind, name="lens_summary_v1.json"):
doc=fixture_doc(name),
commit="a" * 40,
fetched_at="2026-10-04T00:00:00Z",
refreshed_at="2026-10-04T00:00:00Z",
)


Expand Down Expand Up @@ -80,3 +81,19 @@ def test_render_respects_supplied_clock_for_freshness(registry_file, fake_mind):
s.doc["valid_until"] = "2020-01-03T00:00:00Z"
assert "valid · deadline" in board.render_html([s], now="2020-01-02T00:00:00Z")
assert "stale · deadline" in board.render_html([s], now="2020-01-04T00:00:00Z")


def test_capture_freshness_is_conservative(registry_file, fake_mind):
s = view(registry_file, fake_mind)
html = board.render_html([s])
assert 'data-refreshed-at="' + s.refreshed_at + '"' in html
assert (
"https://github.com/PyAutoLabs/PyAutoInsight/actions/workflows/dashboard_refresh.yml"
in html
)
s.outcome = "unavailable"
s.cached = True
assert "Last updated unavailable" in board.render_html([s])
s.outcome = "ok"
s.refreshed_at = None
assert "Last updated unavailable" in board.render_html([s])
12 changes: 10 additions & 2 deletions tests/test_ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,19 @@ def test_ok_reads_the_file_at_the_resolved_commit(lens, web, tmp_path, monkeypat
assert snap["commit"] == SHA_A and snap["fetched_at"] == "2026-10-02T00:00:00Z"


def test_unchanged_reingest_rewrites_nothing(lens, web, tmp_path):
def test_unchanged_refresh_advances_observation_not_capture(lens, web, tmp_path):
web.publish(GH, SHA_A, PATH, fixture_doc("lens_summary_v1.json"))
ingest.ingest(lens, tmp_path, now="2026-10-02T00:00:00Z")
before = {p: p.read_bytes() for p in tmp_path.glob("*s/lens.json")}
ingest.ingest(lens, tmp_path, now="2026-10-03T00:00:00Z")
assert {p: p.read_bytes() for p in tmp_path.glob("*s/lens.json")} == before
assert (
ingest.snapshot_path(tmp_path, "lens").read_bytes()
== before[ingest.snapshot_path(tmp_path, "lens")]
)
replay = ingest.ingest(lens, tmp_path, offline=True)
assert replay.fetched_at == "2026-10-02T00:00:00Z"
assert replay.refreshed_at == "2026-10-03T00:00:00Z"
assert ingest.read_receipt(tmp_path, "lens")["fetched_at"] == "2026-10-02T00:00:00Z"
web.publish(GH, SHA_B, PATH, fixture_doc("lens_summary_v1.json"))
s = ingest.ingest(lens, tmp_path, now="2026-10-04T00:00:00Z")
assert s.commit == SHA_B and s.fetched_at == "2026-10-04T00:00:00Z"
Expand All @@ -53,6 +60,7 @@ def test_unavailable_keeps_the_cached_snapshot_with_its_original_times(lens, web
web.down = True
s = ingest.ingest(lens, tmp_path, now="2026-10-09T00:00:00Z")
assert s.outcome == "unavailable" and s.cached
assert s.refreshed_at is None
assert s.commit == SHA_A and s.fetched_at == "2026-10-02T00:00:00Z"
assert s.doc["evidence_updated_at"] == good.doc["evidence_updated_at"]
assert "connection reset" in s.errors[0]
Expand Down
Loading