@caddis/cli 0.4.60 → 0.4.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -217,8 +217,8 @@ def build(root: Path, slug: str, roadmap_mod, now: datetime.datetime | None = No
|
|
|
217
217
|
|
|
218
218
|
# One colour set for every page: light = slate + blue, dark = Dracula (owner, 17 Oct): https://draculatheme.com/contribute
|
|
219
219
|
# Dark applies when the OS asks for it, unless the reader picked light; the toggle sets data-theme on <html>.
|
|
220
|
-
_PALETTE_LIGHT = '--bg:#f4f6f9;--panel:#fff;--panel2:#f8fafc;--fg:#0f172a;--muted:#475569;--line:#e2e8f0;--line2:#cbd5e1;--accent:#2563eb;--todo-bg:#f1f5f9;--todo-fg:#334155;--prog:#2563eb;--prog-bg:#dbeafe;--prog-fg:#1d4ed8;--done:#16a34a;--done-bg:#dcfce7;--done-fg:#15803d;--block:#d97706;--block-bg:#fef3c7;--block-fg:#92400e;--high:#dc2626;--wait-bg:#fee2e2;--wait-fg:#b91c1c;--nav:#f8fafc;--nav-fg:#0f172a;--on-accent:#fff;--shadow:0 1px 3px rgba(15,23,42,.06);--title:var(--fg);--summary:var(--fg);--code:var(--fg);--sans:"Maple Mono","Segoe UI Variable","Segoe UI",system-ui,sans-serif;--mono:"JetBrains Mono","Cascadia Mono",Consolas,ui-monospace,monospace;color-scheme:light;'
|
|
221
|
-
_PALETTE_DARK = '--bg:#282a36;--panel:#21222c;--panel2:#343746;--fg:#f8f8f2;--muted:#6272a4;--line:#44475a;--line2:#6272a4;--accent:#8be9fd;--todo-bg:#44475a;--todo-fg:#f8f8f2;--prog:#bd93f9;--prog-bg:#3c3352;--prog-fg:#bd93f9;--done:#50fa7b;--done-bg:#2b4a36;--done-fg:#50fa7b;--block:#ffb86c;--block-bg:#4a3a2b;--block-fg:#ffb86c;--high:#ff5555;--wait-bg:#4a2b33;--wait-fg:#ff5555;--nav:#21222c;--nav-fg:#f8f8f2;--on-accent:#282a36;--shadow:none;--title:#bd93f9;--summary:#ff79c6;--code:#f1fa8c;color-scheme:dark;'
|
|
220
|
+
_PALETTE_LIGHT = '--bg:#f4f6f9;--panel:#fff;--panel2:#f8fafc;--fg:#0f172a;--muted:#475569;--line:#e2e8f0;--line2:#cbd5e1;--accent:#2563eb;--todo-bg:#f1f5f9;--todo-fg:#334155;--prog:#2563eb;--prog-bg:#dbeafe;--prog-fg:#1d4ed8;--done:#16a34a;--done-bg:#dcfce7;--done-fg:#15803d;--block:#d97706;--block-bg:#fef3c7;--block-fg:#92400e;--high:#dc2626;--wait-bg:#fee2e2;--wait-fg:#b91c1c;--nav:#f8fafc;--nav-fg:#0f172a;--on-accent:#fff;--shadow:0 1px 3px rgba(15,23,42,.06);--title:var(--fg);--summary:var(--fg);--code:var(--fg);--c0:#2563eb;--c1:#7c3aed;--c2:#0d9488;--c3:#ea580c;--c4:#db2777;--c5:#475569;--sans:"Maple Mono","Segoe UI Variable","Segoe UI",system-ui,sans-serif;--mono:"JetBrains Mono","Cascadia Mono",Consolas,ui-monospace,monospace;color-scheme:light;'
|
|
221
|
+
_PALETTE_DARK = '--bg:#282a36;--panel:#21222c;--panel2:#343746;--fg:#f8f8f2;--muted:#6272a4;--line:#44475a;--line2:#6272a4;--accent:#8be9fd;--todo-bg:#44475a;--todo-fg:#f8f8f2;--prog:#bd93f9;--prog-bg:#3c3352;--prog-fg:#bd93f9;--done:#50fa7b;--done-bg:#2b4a36;--done-fg:#50fa7b;--block:#ffb86c;--block-bg:#4a3a2b;--block-fg:#ffb86c;--high:#ff5555;--wait-bg:#4a2b33;--wait-fg:#ff5555;--nav:#21222c;--nav-fg:#f8f8f2;--on-accent:#282a36;--shadow:none;--title:#bd93f9;--summary:#ff79c6;--code:#f1fa8c;--c0:#8be9fd;--c1:#bd93f9;--c2:#50fa7b;--c3:#ffb86c;--c4:#ff79c6;--c5:#6272a4;color-scheme:dark;'
|
|
222
222
|
_THEME_CSS = (":root{" + _PALETTE_LIGHT + "}" + '@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){'
|
|
223
223
|
+ _PALETTE_DARK + "}}" + ':root[data-theme="dark"]{' + _PALETTE_DARK + "}")
|
|
224
224
|
|
|
@@ -230,7 +230,7 @@ h1{font-size:24px;margin:0;color:var(--title)}h2{font-size:16px;margin:0}.sub{co
|
|
|
230
230
|
.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 16px}
|
|
231
231
|
details.card>summary{cursor:pointer;font-weight:700;font-size:16px;list-style:none;color:var(--summary)}
|
|
232
232
|
details.card>summary::-webkit-details-marker{display:none}details.card>summary::before{content:"\\25B8 ";color:var(--muted)}
|
|
233
|
-
details.card[open]>summary::before{content:"\\25BE "}details.card
|
|
233
|
+
details.card[open]>summary::before{content:"\\25BE "}details.card+details.card{margin-top:8px}
|
|
234
234
|
.wait{border-color:var(--high)}.wait h2{color:var(--wait-fg)}.empty{color:var(--muted)}
|
|
235
235
|
table{border-collapse:collapse;width:100%}th,td{text-align:left;padding:7px 9px;border-top:1px solid var(--line);
|
|
236
236
|
vertical-align:top}th{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);border-top:0}
|
|
@@ -239,15 +239,21 @@ padding:1px 8px;border-radius:999px;white-space:nowrap}
|
|
|
239
239
|
.s-todo{background:var(--todo-bg);color:var(--todo-fg)}.s-in-progress{background:var(--prog-bg);color:var(--prog-fg)}
|
|
240
240
|
.s-done,.s-moved,.s-not-needed{background:var(--done-bg);color:var(--done-fg)}.s-blocked{background:var(--block-bg);color:var(--block-fg)}
|
|
241
241
|
.s-code-done\\/data-pending{background:var(--block-bg);color:var(--block-fg)}
|
|
242
|
-
.lanes{display:grid
|
|
242
|
+
.lanes{display:grid}.lanes>div{border-top:1px solid var(--line);border-left:1px dashed var(--line);
|
|
243
243
|
padding:6px}.lanes .hd{background:var(--panel2);border-top:0;font-weight:700}.lanes .ln{position:sticky;left:0;z-index:1;
|
|
244
|
-
background:var(--panel);border-left:
|
|
245
|
-
.tile{display:flex;gap:
|
|
244
|
+
background:var(--panel);border-left:4px solid var(--lc,var(--line2));border-right:1px solid var(--line2);font-weight:600;overflow-wrap:anywhere}
|
|
245
|
+
.tile{display:flex;gap:5px;align-items:center;font-size:12px;padding:3px 5px;position:relative;margin:0 0 4px;border-radius:5px;
|
|
246
246
|
border:1px solid var(--line2);background:var(--panel);max-width:100%}.tile b{font:600 11px var(--mono)}
|
|
247
|
-
.tile span{
|
|
247
|
+
.tile span{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;white-space:normal;overflow-wrap:anywhere}.tile.in-progress{background:var(--prog-bg);
|
|
248
248
|
border-color:var(--prog);color:var(--prog-fg)}.tile.done{background:var(--done-bg);border-color:var(--done);
|
|
249
249
|
color:var(--done-fg)}.tile.moved{background:repeating-linear-gradient(135deg,var(--done-bg) 0 5px,var(--panel) 5px 10px);
|
|
250
250
|
border-color:var(--done)}.tile.blocked{background:var(--block-bg);border-color:var(--block);color:var(--block-fg)}
|
|
251
|
+
.tile.hi::after{position:absolute;top:3px;right:3px;margin:0}
|
|
252
|
+
.ltile{display:flex;gap:8px;align-items:baseline;font-size:13px;padding:5px 8px;margin:0 0 4px;border-radius:6px;border:1px solid var(--line2);background:var(--panel)}.ltile b{font:600 11px var(--mono);white-space:nowrap}
|
|
253
|
+
.ltile.in-progress{background:var(--prog-bg);border-color:var(--prog);color:var(--prog-fg)}.ltile.done{background:var(--done-bg);border-color:var(--done);color:var(--done-fg)}
|
|
254
|
+
.ltile.moved{background:repeating-linear-gradient(135deg,var(--done-bg) 0 5px,var(--panel) 5px 10px);border-color:var(--done)}.ltile.blocked{background:var(--block-bg);border-color:var(--block);color:var(--block-fg)}
|
|
255
|
+
.bcount{display:inline-block;font-size:11px;font-weight:700;color:var(--block-fg);background:var(--block-bg);border:1px dashed var(--block);border-radius:6px;padding:0 6px;margin-top:3px}
|
|
256
|
+
.lane-list{display:none}.lane-list h3{margin:10px 0 6px}@media (max-width:900px){.timeline .scroll{display:none}.lane-list{display:block}}
|
|
251
257
|
.hi::after{content:"";width:7px;height:7px;border-radius:50%;background:var(--high);flex:none;margin-left:auto}
|
|
252
258
|
.gate{font-size:11px;color:var(--block-fg);background:var(--block-bg);border:1px dashed var(--block);border-radius:6px;
|
|
253
259
|
padding:3px 6px;margin:2px 0;white-space:normal;overflow-wrap:anywhere}.legend{display:flex;flex-wrap:wrap;gap:12px;font-size:12px;color:var(--muted);margin-top:8px}
|
|
@@ -257,6 +263,11 @@ padding:3px 6px;margin:2px 0;white-space:normal;overflow-wrap:anywhere}.legend{d
|
|
|
257
263
|
.bar i{display:block;height:100%;background:var(--done)}a{color:var(--accent)}td.num{white-space:nowrap}
|
|
258
264
|
.theme-btn{font:600 12px var(--sans);padding:6px 11px;border-radius:999px;border:1px solid var(--line);background:var(--panel2);color:var(--fg);cursor:pointer}
|
|
259
265
|
.hdr{display:flex;justify-content:space-between;align-items:flex-start;gap:12px}
|
|
266
|
+
.chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}.chip{border:1px solid var(--line);border-left:4px solid var(--lc);border-radius:8px;padding:5px 10px;background:var(--panel2);font-size:12.5px}.chip span{color:var(--muted);font-family:var(--mono)}
|
|
267
|
+
.flow{display:flex;gap:6px;align-items:stretch;min-width:max-content}.fnode{display:grid;gap:3px;padding:8px 10px;border:1px solid var(--line);border-radius:9px;background:var(--panel2);text-decoration:none;color:inherit;min-width:120px}.fnode span{font:11px var(--mono);color:var(--muted)}.fnode span.pill{font:700 11px var(--sans);justify-self:start}.arrow{align-self:center;color:var(--muted)}
|
|
268
|
+
.kind{font-size:10.5px;font-weight:700;text-transform:uppercase;padding:2px 8px;border-radius:6px;background:var(--block-bg);color:var(--block-fg)}.k-owner-gate{background:var(--wait-bg);color:var(--wait-fg)}
|
|
269
|
+
.note{color:var(--muted);font-weight:400;font-size:13px}.sec-h{display:flex;flex-wrap:wrap;gap:4px 10px;align-items:baseline;margin-bottom:8px}h3{font-size:14px;margin:0 0 8px}
|
|
270
|
+
.cols .rcard{margin-bottom:8px}.cols .rcard .bar{margin:0}
|
|
260
271
|
"""
|
|
261
272
|
|
|
262
273
|
_CSS = _THEME_CSS + _BASE_CSS
|
|
@@ -285,7 +296,7 @@ _SHELL_CSS = """
|
|
|
285
296
|
.main{min-width:0}.top{display:flex;align-items:center;gap:12px;padding:10px 20px;border-bottom:1px solid var(--line);background:var(--panel);position:sticky;top:0;z-index:5}
|
|
286
297
|
.crumbs{flex:1;min-width:0;font-size:13px;color:var(--muted)}.crumbs a{color:var(--accent);text-decoration:none}
|
|
287
298
|
main [id]{scroll-margin-top:60px}main>.view{scroll-margin-top:200px}
|
|
288
|
-
.view{display:none}.view:target,.view:has(:target){display:block}main:not(:has(.view:target,.view :target))>#inbox{display:block}
|
|
299
|
+
.view>*+*{margin-top:16px}.view{display:none}.view:target,.view:has(:target){display:block}main:not(:has(.view:target,.view :target))>#inbox{display:block}
|
|
289
300
|
.nav-sub{display:none}.view-crumbs{font-size:13px;color:var(--muted);margin:0 0 8px}
|
|
290
301
|
.nav-cb{display:none}.nav-btn,.nav-close,.nav-scrim{display:none}
|
|
291
302
|
.nav-cb:focus-visible~.app .nav-btn{outline:2px solid var(--accent);outline-offset:2px}
|
|
@@ -430,20 +441,24 @@ def _waiting_section(model: dict) -> str:
|
|
|
430
441
|
def _swimlane(model: dict) -> str:
|
|
431
442
|
milestones, lanes = model["milestones"], model["lanes"]
|
|
432
443
|
cols = len(milestones)
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
444
|
+
# The section has the class `timeline` as well as the id: the hub prefixes every id, so the phone CSS selects by class.
|
|
445
|
+
out = ['<section class="card timeline" id="timeline"><div class="sec-h"><h2>Timeline</h2><span class="note">plans as '
|
|
446
|
+
'lanes, milestones as columns in roadmap order, one tile per phase</span></div><div class="scroll">',
|
|
447
|
+
f'<div class="lanes" style="grid-template-columns:150px repeat({cols},minmax(110px,1fr));width:100%;'
|
|
448
|
+
f'min-width:{150 + cols * 110}px">',
|
|
436
449
|
'<div class="hd ln">Plan / milestone</div>']
|
|
437
450
|
for ms in milestones:
|
|
438
451
|
dep = f"after {', '.join(ms['depends'])}" if ms["depends"] else "starts first"
|
|
439
|
-
|
|
452
|
+
n = len(ms["blockers"])
|
|
453
|
+
count = f' <span class="bcount">{n} gate(s)</span>' if n else ""
|
|
440
454
|
out.append(f'<div class="hd">{_e(ms["id"])} {_pill(ms["status"])}<div class="empty" style="font-weight:400;'
|
|
441
|
-
f'font-size:11px">{_e(dep)}
|
|
442
|
-
|
|
455
|
+
f'font-size:11px">{_e(dep)}</div>{count}</div>')
|
|
456
|
+
plan_lanes = lanes or ["-"]
|
|
457
|
+
for i, lane in enumerate(plan_lanes):
|
|
443
458
|
done = sum(1 for ms in milestones for ph in ms["phases"] if ph["plan"] == lane and ph["tile"] in ("done", "moved"))
|
|
444
459
|
total = sum(1 for ms in milestones for ph in ms["phases"] if ph["plan"] == lane)
|
|
445
|
-
out.append(f'<div class="ln">{_e(lane)}<div class="empty"
|
|
446
|
-
f'{done}/{total} done</div></div>')
|
|
460
|
+
out.append(f'<div class="ln" style="--lc:var(--c{i % 6})">{_e(lane)}<div class="empty" '
|
|
461
|
+
f'style="font-weight:400;font-size:11px">{done}/{total} done</div></div>')
|
|
447
462
|
for ms in milestones:
|
|
448
463
|
tiles = []
|
|
449
464
|
for ph in ms["phases"]:
|
|
@@ -454,8 +469,24 @@ def _swimlane(model: dict) -> str:
|
|
|
454
469
|
tiles.append(f'<div class="tile {_e(ph["tile"])}{hi}" title="{_e(title)}"><b>{_e(ph["num"])}</b>'
|
|
455
470
|
f'<span>{_e(ph["name"] or ph["ref"])}</span></div>')
|
|
456
471
|
out.append(f"<div>{''.join(tiles)}</div>")
|
|
457
|
-
|
|
458
|
-
|
|
472
|
+
if any(ms["blockers"] for ms in milestones): # the blockers get their own lane, so the column headers stay short
|
|
473
|
+
out.append('<div class="ln">Owner and outside</div>')
|
|
474
|
+
for ms in milestones:
|
|
475
|
+
cells = []
|
|
476
|
+
for b in ms["blockers"]:
|
|
477
|
+
reason = str(b["reason"])
|
|
478
|
+
short = reason if len(reason) <= 60 else reason[:60] + "…"
|
|
479
|
+
cells.append(f'<div class="gate" title="{_e(reason)}">{_e(b["type"])}: {_e(short)}</div>')
|
|
480
|
+
out.append(f"<div>{''.join(cells)}</div>")
|
|
481
|
+
out.append('</div></div><div class="lane-list">')
|
|
482
|
+
for ms in milestones: # the phone list: the grid is hidden on a narrow screen
|
|
483
|
+
out.append(f'<h3>{_e(ms["id"])} {_pill(ms["status"])}</h3>')
|
|
484
|
+
for ph in ms["phases"]:
|
|
485
|
+
hi = " hi" if "high" in ph.get("risk", "").lower() else ""
|
|
486
|
+
out.append(f'<div class="ltile {_e(ph["tile"])}{hi}"><b>{_e(ph["plan"])} {_e(ph["num"])}</b>'
|
|
487
|
+
f'<span>{_e(ph["name"] or ph["ref"])}</span></div>')
|
|
488
|
+
out.append('</div><div class="legend"><span>colour: to do · <b>in progress</b> · done · striped = moved · '
|
|
489
|
+
'amber = blocked</span><span>red dot = Risk high (stops for review)</span></div></section>')
|
|
459
490
|
return "".join(out)
|
|
460
491
|
|
|
461
492
|
|
|
@@ -466,14 +497,29 @@ def _summary(model: dict) -> str:
|
|
|
466
497
|
high = sum(1 for ph in phases if "high" in ph.get("risk", "").lower())
|
|
467
498
|
nxt = model.get("next") or {}
|
|
468
499
|
nxt_txt = f"{nxt.get('milestone', '')} {nxt.get('plan', '')}#{nxt.get('phase', '')}" if nxt else "none ready"
|
|
469
|
-
return ('<
|
|
470
|
-
'<div class="kpis">'
|
|
500
|
+
return ('<section class="kpis" id="overview" aria-label="Summary">'
|
|
471
501
|
f'<div class="kpi"><b>{_e(done)} / {_e(len(phases))}</b>phases done</div>'
|
|
472
502
|
f'<div class="kpi"><b>{_e(model["done"])} / {_e(model["total"])}</b>milestones done</div>'
|
|
473
503
|
f'<div class="kpi"><b>{_e(len(building))}</b>in progress: {_e(", ".join(building) or "none")}</div>'
|
|
474
504
|
f'<div class="kpi"><b>{_e(high)}</b>high-risk phases</div>'
|
|
475
505
|
f'<div class="kpi"><b>{_e(nxt_txt)}</b>next batch for the runner</div>'
|
|
476
|
-
"</
|
|
506
|
+
"</section>")
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _done_phases(ms: dict) -> int:
|
|
510
|
+
return sum(1 for ph in ms["phases"] if ph["tile"] in ("done", "moved"))
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _flow(model: dict) -> str:
|
|
514
|
+
"""The milestones as a left-to-right chain in roadmap order; each node links to its milestone below."""
|
|
515
|
+
nodes = []
|
|
516
|
+
for ms in model["milestones"]:
|
|
517
|
+
dep = f"after {', '.join(ms['depends'])}" if ms["depends"] else "starts first"
|
|
518
|
+
nodes.append(f'<a class="fnode s-{_e(ms["status"])}" href="#ms-{_e(ms["id"])}"><b>{_e(ms["id"])}</b> '
|
|
519
|
+
f'{_pill(ms["status"])}<span>{_e(dep)} · {_e(_done_phases(ms))}/{_e(len(ms["phases"]))} ph</span></a>')
|
|
520
|
+
chain = '<span class="arrow" aria-hidden="true">→</span>'.join(nodes)
|
|
521
|
+
return (f'<section id="flow"><div class="sec-h"><h2>Flow</h2><span class="note">milestones in roadmap order</span></div>'
|
|
522
|
+
f'<div class="card scroll"><div class="flow">{chain}</div></div></section>')
|
|
477
523
|
|
|
478
524
|
|
|
479
525
|
def _now_next_later(model: dict) -> str:
|
|
@@ -492,26 +538,30 @@ def _now_next_later(model: dict) -> str:
|
|
|
492
538
|
groups["Later"].append(ms)
|
|
493
539
|
cols = []
|
|
494
540
|
for title, items in groups.items():
|
|
495
|
-
body = "".join(f'<
|
|
496
|
-
f'{
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
541
|
+
body = "".join(f'<a class="rcard" href="#ms-{_e(m["id"])}"><span><strong>{_e(m["id"])}</strong> {_pill(m["status"])}</span>'
|
|
542
|
+
f'{_bar(_done_phases(m), len(m["phases"]))}'
|
|
543
|
+
+ "".join(f'<span class="note">{_e(b["type"])}</span>' for b in m["blockers"])
|
|
544
|
+
+ "</a>" for m in items)
|
|
545
|
+
cols.append(f'<div><h3>{_e(title)}</h3>{body or "<p class=empty>none</p>"}</div>')
|
|
546
|
+
return ('<section id="board"><div class="sec-h"><h2>Now, next, later</h2>'
|
|
547
|
+
'<span class="note">which milestones can start</span></div>'
|
|
548
|
+
f'<div class="cols">{"".join(cols)}</div></section>')
|
|
501
549
|
|
|
502
550
|
|
|
503
551
|
def _gates(model: dict) -> str:
|
|
504
552
|
rows = [(ms["id"], b) for ms in model["milestones"] for b in ms["blockers"]]
|
|
505
|
-
body = "".join(f"<tr><td><strong>{_e(mid)}</strong></td
|
|
553
|
+
body = "".join(f"<tr><td><strong>{_e(mid)}</strong></td>"
|
|
554
|
+
f"<td><span class=\"kind k-{_e(b['type'])}\">{_e(b['type'])}</span></td><td>{_e(b['reason'])}</td>"
|
|
506
555
|
f"<td><code>{_e(b['check'])}</code></td></tr>" for mid, b in rows)
|
|
507
|
-
return (f'<
|
|
556
|
+
return (f'<section class="card" id="gates"><div class="sec-h"><h2>Gates and blockers</h2>'
|
|
557
|
+
f'<span class="note">{_e(len(rows))} open</span></div>'
|
|
508
558
|
+ (f'<div class="scroll"><table><tr><th>Milestone</th><th>Type</th><th>Reason</th><th>Check</th></tr>{body}'
|
|
509
|
-
"</table></div>" if rows else '<p class="empty">No blockers.</p>') + "</
|
|
559
|
+
"</table></div>" if rows else '<p class="empty">No blockers.</p>') + "</section>")
|
|
510
560
|
|
|
511
561
|
|
|
512
562
|
def _milestones(model: dict) -> str:
|
|
513
|
-
out = ['<
|
|
514
|
-
'Tracker status</span></
|
|
563
|
+
out = ['<section id="milestones"><div class="sec-h"><h2>Milestones and phases</h2><span class="note">every phase with '
|
|
564
|
+
'model, lane, risk and Tracker status</span></div>']
|
|
515
565
|
for ms in model["milestones"]:
|
|
516
566
|
rows = "".join(
|
|
517
567
|
f"<tr><td><code>{_e(ph['ref'])}</code></td><td>{_e(ph['name'])}</td><td>{_e(_short_model(ph['model']))}</td>"
|
|
@@ -519,13 +569,13 @@ def _milestones(model: dict) -> str:
|
|
|
519
569
|
f"<td>{_e(ph['risk'] or '-')}</td><td>{_pill(ph['tile'])} {_e(ph['tracker'])}</td></tr>"
|
|
520
570
|
for ph in ms["phases"])
|
|
521
571
|
target = f" · target {_e(ms['target'])}" if ms.get("target") else ""
|
|
522
|
-
out.append(f
|
|
523
|
-
f"<span class
|
|
524
|
-
f
|
|
525
|
-
|
|
572
|
+
out.append(f'<details class="card" id="ms-{_e(ms["id"])}"><summary>{_e(ms["id"])} '
|
|
573
|
+
f'{_pill(ms["status"])} <span class="note">owner {_e(ms["owner"])}{target} · '
|
|
574
|
+
f'after {_e(", ".join(ms["depends"]) or "-")}</span></summary>'
|
|
575
|
+
'<div class="scroll"><table><tr><th>Phase</th><th>Name</th><th>Model</th><th>Lane</th><th>Risk</th>'
|
|
526
576
|
f'<th>Tracker</th></tr>{rows or "<tr><td colspan=6 class=empty>no build phases</td></tr>"}'
|
|
527
|
-
"</table></div>")
|
|
528
|
-
out.append("</
|
|
577
|
+
"</table></div></details>")
|
|
578
|
+
out.append("</section>")
|
|
529
579
|
return "".join(out)
|
|
530
580
|
|
|
531
581
|
|
|
@@ -554,13 +604,13 @@ def _runner(model: dict) -> str:
|
|
|
554
604
|
if nxt:
|
|
555
605
|
parts.append(f"<p>Next batch: <code>{_e(nxt.get('milestone'))} {_e(nxt.get('plan'))}#{_e(nxt.get('phase'))}"
|
|
556
606
|
"</code></p>")
|
|
557
|
-
return ('<
|
|
558
|
-
+ "".join(parts) + "</
|
|
607
|
+
return ('<section class="card" id="runner"><div class="sec-h"><h2>Runner</h2>'
|
|
608
|
+
'<span class="note">roadmap-run state on this machine</span></div>' + "".join(parts) + "</section>")
|
|
559
609
|
|
|
560
610
|
|
|
561
|
-
#
|
|
562
|
-
_PAGE_ANCHORS = [("waiting", "Waiting"), ("
|
|
563
|
-
("milestones", "Milestones"), ("runner", "Runner")]
|
|
611
|
+
# The sections of a roadmap view, in page order: the nav sub-links. `render_body` writes them in this order.
|
|
612
|
+
_PAGE_ANCHORS = [("waiting", "Waiting"), ("overview", "Overview"), ("flow", "Flow"), ("board", "Board"),
|
|
613
|
+
("timeline", "Timeline"), ("gates", "Gates"), ("milestones", "Milestones"), ("runner", "Runner")]
|
|
564
614
|
|
|
565
615
|
def _rid(folder, slug) -> str | None:
|
|
566
616
|
"""The view id of a roadmap in `index.html`: `rm-<the folder's 6 hex>-<slug>` (the 6 hex keep two clones with
|
|
@@ -602,12 +652,18 @@ def render_body(model: dict) -> str:
|
|
|
602
652
|
phases = [ph for ms in model["milestones"] for ph in ms["phases"]]
|
|
603
653
|
done = sum(1 for ph in phases if ph["tile"] in ("done", "moved"))
|
|
604
654
|
commit = f" · origin <code>{_e(model.get('origin_commit'))}</code>" if model.get("origin_commit") else ""
|
|
655
|
+
chips = "".join(
|
|
656
|
+
f'<span class="chip" style="--lc:var(--c{i % 6})"><b>{_e(plan)}</b> <span>'
|
|
657
|
+
f'{_e(sum(1 for ph in phases if ph["plan"] == plan and ph["tile"] in ("done", "moved")))}/'
|
|
658
|
+
f'{_e(sum(1 for ph in phases if ph["plan"] == plan))} phases</span></span>'
|
|
659
|
+
for i, plan in enumerate(model["lanes"]))
|
|
605
660
|
return "".join([
|
|
606
661
|
f'<header class="hdr"><div><h1>{_e(title)}</h1><p class="sub">{_e(model["done"])} of {_e(model["total"])} '
|
|
607
662
|
f'milestones · {_e(done)} of {_e(len(phases))} phases done · branch <code>{_e(model["branch"])}</code>'
|
|
608
|
-
f
|
|
609
|
-
|
|
610
|
-
|
|
663
|
+
f'{commit} · generated {_e(model["generated"])}</p>'
|
|
664
|
+
+ (f'<div class="chips">{chips}</div>' if chips else "") + "</div></header>",
|
|
665
|
+
_waiting_section(model), _summary(model), _flow(model), _now_next_later(model), _swimlane(model),
|
|
666
|
+
_gates(model), _milestones(model), _runner(model),
|
|
611
667
|
'<p class="empty">Built from roadmap.md, the plan Trackers and headers on origin, the runner state and the '
|
|
612
668
|
"session-state folder. Re-rendered by the runner, /handoff, /pr-merge, /catchup and the waiting hooks.</p>"])
|
|
613
669
|
|
package/bundles/manifest.json
CHANGED
package/package.json
CHANGED