diff --git a/scripts/board.py b/scripts/board.py index ce0b7d9..30c4f45 100644 --- a/scripts/board.py +++ b/scripts/board.py @@ -240,6 +240,34 @@ def _queue_issue_url(snapshot: dict, section: str, paper: dict, f"&body={_quote(body, safe='')}&labels={_quote(label, safe='')}") +def _read_trend(snapshot: dict) -> dict: + """Read counts from the DONE history, relative to the snapshot's + `generated` timestamp (pure — no wall clock): papers read in the last + 7/30 days plus eight weekly buckets, oldest first.""" + trend = {"d7": 0, "d30": 0, "weeks": [0] * 8} + try: + now = datetime.date.fromisoformat(str(snapshot.get("generated"))[:10]) + except ValueError: + return trend + for q in snapshot.get("queue") or []: + for p in q.get("papers") or []: + if not p.get("done_date"): + continue + try: + age = (now - datetime.date.fromisoformat(p["done_date"])).days + except ValueError: + continue + if age < 0: + continue + if age < 7: + trend["d7"] += 1 + if age < 30: + trend["d30"] += 1 + if age < 56: + trend["weeks"][7 - age // 7] += 1 + return trend + + def _totals(snapshot: dict) -> dict: wikis = snapshot.get("wikis") or [] statuses: dict[str, int] = {} @@ -313,6 +341,10 @@ def _render_md(snapshot: dict) -> str: f"{s.get('drafted', 0)} | {s.get('reviewed', 0)} | " f"{w.get('todo', 0)} |") lines += ["", "## Reading queue", ""] + trend = _read_trend(snapshot) + if sum(q.get("done", 0) for q in snapshot.get("queue") or []): + lines += [f"_{trend['d7']} read in the last 7 days · " + f"{trend['d30']} in the last 30._", ""] for q in snapshot.get("queue") or []: done = f", {q['done']} read" if q.get("done") else "" lines += [f"
{q['section']}: {q['count']} " @@ -411,6 +443,26 @@ def _render_html(snapshot: dict) -> str: if not queue_blocks: queue_blocks.append("

queue empty or unavailable

") + trend = _read_trend(snapshot) + total_done = sum(q.get("done", 0) for q in snapshot.get("queue") or []) + trend_bits = f"{t['queued']} papers waiting" + spark = "" + if total_done: + trend_bits += (f" · {trend['d7']} read last 7d · " + f"{trend['d30']} last 30d") + if sum(trend["weeks"]): + mx = max(trend["weeks"]) + bars = "".join( + f"" for w in trend["weeks"]) + spark = (f" {bars}") + filter_box = "" + if t["queued"]: + filter_box = (f"") + todo_rows = [] for w in snapshot.get("wikis") or []: if not w.get("todo"): @@ -482,6 +534,13 @@ def _render_html(snapshot: dict) -> str: a.act:hover {{ background: #30363d; text-decoration: none; }} details.hist {{ margin: .25rem 0 .25rem 1.3rem; }} details.hist > summary {{ cursor: pointer; }} + #pfilter {{ width: 100%; padding: .45rem .6rem; margin: .25rem 0 .5rem; + background: #0d1117; color: #c9d1d9; border: 1px solid #30363d; + border-radius: 6px; font: inherit; }} + .spark {{ display: inline-flex; align-items: flex-end; gap: 2px; height: 12px; + margin-left: .45rem; }} + .spark .sb {{ width: 5px; background: #8957e5; border-radius: 1px; + display: inline-block; }} footer {{ margin-top: 2rem; color: #8b949e; font-size: .8rem; }}

PyAutoMemory Dashboard

{t['pages']} pages · {pct}% cited markdown version

On a paper: 📥 intake · 📑 cite · ✅ mark read — each opens a prefilled issue (add notes, submit to act). 📋 copies a Claude Code prompt.

-

Reading queue ({t['queued']} papers waiting)

+

Reading queue ({trend_bits}){spark}

+ {filter_box} {''.join(queue_blocks)}

Citation work queue ({t['todo']} sections need a canonical key)

{''.join(todo_rows)}
diff --git a/tests/test_board.py b/tests/test_board.py index a7db9b7..bb621c2 100644 --- a/tests/test_board.py +++ b/tests/test_board.py @@ -130,6 +130,24 @@ def test_md_lists_papers(tmp_path): assert "labels=queue-cite" in md +def test_read_trend_from_done_history(tmp_path): + snap = board.collect(_tree(tmp_path)) + # fixture DONE date is 2026-06-01; pin "now" via the generated stamp + snap["generated"] = "2026-06-10T00:00:00+00:00" + trend = board._read_trend(snap) + assert trend == {"d7": 0, "d30": 1, + "weeks": [0, 0, 0, 0, 0, 0, 1, 0]} # age 9d → bucket 6 + html = board.render(snap, "html") + assert "0 read last 7d · 1 last 30d" in html + assert "class='spark'" in html + assert "1 in the last 30." in board.render(snap, "md") + + +def test_filter_box_markup(tmp_path): + html = board.render(board.collect(_tree(tmp_path)), "html") + assert "id='pfilter'" in html and "function flt(" in html + + def test_md_brief_is_one_line(tmp_path): out = board.render(board.collect(_tree(tmp_path)), "md-brief") assert "\n" not in out and "pages" in out