diff --git a/CHANGELOG.md b/CHANGELOG.md index edafc87e..b5a253d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m ## Unreleased +- fix(progress-report): **the build plan is shown once: the phase cards are the plan.** Expanding a card now shows its full rows (#, Kind, Step, Produces/Proves, Depends on, State, Scripts) and Claims, which only the duplicate "Full plan tables" block carried; that block is gone and views A (script folders) and B (module proof) are their own section. The `now` phase renders open with a selected style, opening another phase closes the previous one (accordion; `#phase-N` links still open their card), and a viewer's saved open/closed state survives the 30 s reload and wins over the default (#474) — field feedback from a requirements-driven project at Stage 5 — MendixMau (field feedback 2026-10-20) + ## v2026.10.19 - new(progress-report): **the progress report gains a Project pages section: a compact folder tree of the HTML pages found in the project's output folders (gate dashboard, analysis reports, blueprint, design system, process map, wireframes, UI reviews, test reports), each a relative link from `architecture/` with its modified date.** Pages are found by scanning the project's files on every render, never from a stored list; only the top-level folders in `PAGE_DIRS` are scanned (each named with its producer; a symlinked one is skipped), `node_modules`, `.mpr-snapshots`, `deployment`, `mprcontents`, `theme`, `.git`, vendor and dot folders are never entered, a folder with more than 8 pages folds behind a count, every folder keeps its row and lists at most 100 links (`+K more`), and the report never lists itself. Dates are shown as days, so the section is unchanged on the same day unless a listed page changed and a `--refresh` leaves the page alone. Test: `tests/wave2/test-progress-report-pages.sh` (single and two-tree layouts, Windows hrefs) — MendixMau (request 2026-10-19) diff --git a/project-bin/build-plan-status.sh b/project-bin/build-plan-status.sh index b9530361..c7f1ac32 100755 --- a/project-bin/build-plan-status.sh +++ b/project-bin/build-plan-status.sh @@ -532,10 +532,10 @@ render_plan_section() { # stdout: the plan section's HTML echo "
Nothing to show yet: ${PLAN_MSG:-build-plan.md was not read}.
" return 0 fi - "$PY" - "$PLAN_JSON" "$MDLSOURCE" <<'PYEOF' || echo "Could not render the plan section from build-plan.md.
" + "$PY" - "$PLAN_JSON" "$MDLSOURCE" "${PHASE_DETAIL:-/dev/null}" <<'PYEOF' || echo "Could not render the plan section from build-plan.md.
" import fnmatch, html, json, os, re, sys -src, mdl = sys.argv[1], sys.argv[2] +src, mdl, detail_out = sys.argv[1], sys.argv[2], sys.argv[3] d = json.load(open(src, encoding="utf-8")) e = lambda s: html.escape(s or "", quote=True) # `code` spans in plan cells render as, not literal backticks
@@ -631,29 +631,32 @@ if named_total:
print("Scripts named in the plan: %d, of which %d are renamed done- under mdlsource/.%s
" % (
named_total, named_done,
(" ⚠ %s where the plan and the disk disagree." % plural(drift_rows, "row")) if drift_rows else ""))
+# One view of the plan: each phase's rows and claims go to the detail file, which progress-report.py
+# splices into that phase's card. They are not printed here, or the rows would appear twice.
+det = []
for p, out in zip(phases, rows_html):
- print('Phase %s — %s %s
' % (
- e(p["id"]), e(p["name"]), CLS.get(p["state"], "st-unknown"), e(p["state"])))
+ h = ['' % e(p["id"]),
+ 'Phase state: %s
' % (CLS.get(p["state"], "st-unknown"), e(p["state"]))]
if p.get("note"):
- print("%s
" % e(p["note"]))
+ h.append("%s
" % e(p["note"]))
if p["steps"]:
- b = sum(1 for s in p["steps"] if classify(s["state"]) == "built")
- print("%d of %s built
" % (b, plural(len(p["steps"]), "row")))
- print("# Kind Step Produces / Proves "
- "Depends on State Scripts ")
- print("\n".join(out))
- print("
")
+ h.append("# Kind Step Produces / Proves "
+ "Depends on State Scripts ")
+ h.append("\n".join(out))
+ h.append("
")
else:
- print("No row table under this phase.
")
+ h.append("No row table under this phase.
")
if p.get("claims"):
- print("Claims (%d)
" % len(p["claims"]))
+ h.append("Claims (%d)
" % len(p["claims"]))
for c in p["claims"]:
extra = (" (%d)" % c["count"] if c.get("count") is not None else "") + (" [%s]" % c["brd"] if c.get("brd") else "")
- print("%s%s " % (e(c["pointer"]), e(extra)))
- print("
")
+ h.append("%s%s " % (e(c["pointer"]), e(extra)))
+ h.append("
")
elif p.get("claimsNote"):
- print("Claims: %s
" % e(p["claimsNote"]))
- print(" ")
+ h.append("Claims: %s
" % e(p["claimsNote"]))
+ h.append('')
+ det.append("\n".join(h))
+open(detail_out, "w", encoding="utf-8").write("\n".join(det))
w = d.get("warnings") or []
if w:
print("Parser notes (%d): lines in build-plan.md this page could not read as plan data
" % len(w))
@@ -694,6 +697,7 @@ if [ "$WRITE_HTML" -eq 1 ]; then
mkdir -p "$ROOT/architecture"
OUT_TMP="$OUT.tmp.$$"
FRAG_TMP="$OUT.frag.$$"
+ PHASE_DETAIL="$OUT.phases.$$"
{
render_plan_section
@@ -726,6 +730,10 @@ if [ "$WRITE_HTML" -eq 1 ]; then
echo "The plan: what build-plan.md says. A: built, from mdlsource/ done- prefixes. B: proven, from verify-module.sh + docs/improvement-register.md. A phase at 100% with no reviewed row in B is built, not proven.
"
} > "$FRAG_TMP"
GEN_ARGS=(--out "$OUT_TMP" --fragment "$FRAG_TMP" --plan-msg "${PLAN_MSG:-}")
+ # An older progress-report.py (a `sync-project.sh --only-bin build-plan-status.sh` run, or a hand-edited
+ # copy) has no --phase-detail and would exit "unrecognized arguments", writing no page at all. Pass the
+ # flag only when the generator knows it; without it the phase cards keep their short row tables.
+ grep -q -- '--phase-detail' "$GEN" 2>/dev/null && GEN_ARGS+=(--phase-detail "$PHASE_DETAIL")
[ -n "$PLAN_JSON" ] && GEN_ARGS+=(--plan-json "$PLAN_JSON")
for _sl in ${SESSION_LOGS[@]+"${SESSION_LOGS[@]}"}; do GEN_ARGS+=(--session-log "$_sl"); done
if "$PY" "$GEN" "$ROOT" "${GEN_ARGS[@]}" >/dev/null 2>"$OUT.err.$$"; then
@@ -744,7 +752,7 @@ if [ "$WRITE_HTML" -eq 1 ]; then
if [ "$QUIET" -eq 0 ]; then echo "" && echo " $REPORT_FILE not written: the generator failed: $(head -c 300 "$OUT.err.$$" 2>/dev/null)"
else echo "WARN: $REPORT_FILE not updated, progress-report.py failed: ${_why:-no message}" >&2; fi # unattended callers stay quiet on success, never on a crash
fi
- rm -f "${FRAG_TMP:?}" "${OUT}.err.$$"
+ rm -f "${FRAG_TMP:?}" "${PHASE_DETAIL:?}" "${OUT}.err.$$"
fi
fi
diff --git a/project-bin/progress-report.py b/project-bin/progress-report.py
index 88b51bda..951675d7 100755
--- a/project-bin/progress-report.py
+++ b/project-bin/progress-report.py
@@ -1392,7 +1392,7 @@ def rule(ratio, kind, title, seen, matters, fix, why, lost_s=0.0, lost_tok=0.0,
health = run_health(cs)
return fired[:ADV["max_items"]], quiet + fired[ADV["max_items"]:], health, cs, hidden[0]
-JS = ""
+JS = ""
# ----------------------------------------------------------------- render
CSS = """
:root{--bg:#f6f7f9;--card:#fff;--ink:#1c2430;--mut:#667085;--line:#e3e7ee;--accent:#2f6fed;--ok:#1f9d63;--warn:#d98a0b;--bad:#c93c3c;--idle:#d9dde5;--main:#2f6fed;--sub:#8b5cf6;--soft:#eef2fb;--shadow:0 1px 2px rgba(20,30,50,.06)}
@@ -1441,14 +1441,14 @@ def rule(ratio, kind, title, seen, matters, fix, why, lost_s=0.0, lost_tok=0.0,
.layout{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:16px;align-items:start}.layout.solo{display:block}.content{min-width:0}.side{position:sticky;top:52px;max-height:calc(100vh - 64px);overflow:auto;padding-right:2px}.side h2{margin-top:0}.health{margin-top:8px}.health .hi{font-size:13px;margin-top:3px}.grp{margin-top:8px;border:1px solid var(--line);border-radius:10px;background:var(--card)}.grp>summary{cursor:pointer;padding:9px 12px;font-size:14px}.grp .speed,.grp>.split,.grp>div,.grp .tip{padding:0 12px 10px}.tip{border-top:1px solid var(--line);padding-top:10px}.tip h3{font-size:13px;margin:0 0 4px}.tip p{margin:0 0 4px;font-size:12.5px}.p-info{background:#8793a6;color:#fff}@media(max-width:1000px){.layout{grid-template-columns:minmax(0,1fr)}.side{position:static;max-height:none}}.dot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:4px}.p-green{background:#2e9e5b;color:#fff}.p-amber{background:#e0a100;color:#fff}.p-red{background:#d1383d;color:#fff}.pill{padding:1px 9px;margin-left:6px;border-radius:99px;font-size:12px}.speed{margin-top:8px;font-size:13px}.split{display:flex;gap:2px;margin:6px 0}.seg{background:var(--accent);color:#fff;font-size:11px;padding:3px 6px;border-radius:4px;white-space:nowrap;overflow:hidden}.seg:nth-child(2){opacity:.75}.seg:nth-child(3){background:var(--idle);color:var(--ink)}.seg:nth-child(4){opacity:.55}.muted{color:var(--mut);font-size:12px}\n.advl{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:10px;margin-top:8px}
.advi{border-left:4px solid var(--warn)}.advi.hi{border-left-color:var(--bad)}.advi .rk{font-size:12px;color:var(--mut)}.advi h3{margin:2px 0 6px}.advi p{margin:4px 0;font-size:13px}.advi .why{color:var(--mut);font-size:12px}
details.ph>summary{list-style:none;cursor:pointer;color:var(--ink)}details.ph>summary::-webkit-details-marker{display:none}
-details.ph[open]{grid-column:1/-1}.steps td{font-size:12px;padding:3px 6px}
+details.ph[open]{grid-column:1/-1;border-color:var(--accent);box-shadow:0 0 0 1px var(--accent);background:var(--soft)}.steps td{font-size:12px;padding:3px 6px}
.nolog{margin:10px 0;font-size:13px}.note-card{margin-top:14px;border-left:4px solid var(--warn)}.live{font-size:12px;color:var(--muted)}.live a{color:inherit}details.how{margin:10px 0;font-size:13px;color:var(--muted)}details.how summary{cursor:pointer}
-.frag section.phase{margin-top:14px}.frag h2{font-size:16px;margin:24px 0 4px}.frag h3{margin:14px 0 2px}.frag .scroll{overflow-x:auto}
+.frag h2{font-size:16px;margin:24px 0 4px}.frag h3{margin:14px 0 2px}.frag .scroll{overflow-x:auto}
.frag .lede{font-size:14px}.frag .empty{color:var(--mut)}.frag code{font-size:12px}
-.frag .st-built,.frag .done,.frag .CLEAN{color:var(--ok)}.frag .st-progress,.frag .in-progress,.frag .INCOMPLETE{color:var(--warn)}
-.frag .st-todo,.frag .pending{color:var(--mut)}.frag .st-person{color:#8b5cf6}.frag .st-unknown{color:var(--mut);font-style:italic}
-.frag .FINDINGS,.frag .drift{color:var(--bad)}.frag .drift{font-size:12px}.frag .claims{font-size:12px}.frag .warn summary{color:var(--warn)}
-.frag .badge{margin-left:6px;border:1px solid currentColor;background:transparent}
+.frag .st-built,.ph .st-built,.frag .done,.frag .CLEAN{color:var(--ok)}.frag .st-progress,.ph .st-progress,.frag .in-progress,.frag .INCOMPLETE{color:var(--warn)}
+.frag .st-todo,.ph .st-todo,.frag .pending{color:var(--mut)}.frag .st-person,.ph .st-person{color:#8b5cf6}.frag .st-unknown,.ph .st-unknown{color:var(--mut);font-style:italic}
+.frag .FINDINGS,.frag .drift,.ph .drift{color:var(--bad)}.frag .drift,.ph .drift{font-size:12px}.ph .claims{font-size:12px}.frag .claims{font-size:12px}.frag .warn summary{color:var(--warn)}
+.frag .badge,.ph .badge{margin-left:6px;border:1px solid currentColor;background:transparent}
.pages .tree{margin-top:6px;font-size:13px}.pg{display:grid;grid-template-columns:minmax(120px,220px) minmax(0,1fr);gap:2px 12px;padding:5px 0;border-bottom:1px solid var(--line)}.pg:last-child{border-bottom:0}
.pg .dir{font-weight:600;overflow-wrap:anywhere}.pg ul{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:3px 14px}.pg li{overflow-wrap:anywhere}.pg details{margin:0}.pg details ul{margin-top:4px}
footer{margin-top:36px;font-size:13px;color:var(--mut)}footer li{margin:3px 0}
@@ -1739,8 +1739,8 @@ def chip(f, cls=""):
if bp:
kinds = bp["kinds"]; tr = ""
runs_by = {str(k_[0]): d["runs"] for k_, d in (facts["phases"] if facts else []) if k_[0] != ""}
- cur = now_phase(bp, proj)
- w("%d of %d rows built%sJump to the plan tables" %
+ cur = now_phase(bp, proj); phase_detail = args.phase_detail
+ w("%d of %d rows built%sJump to script folders and module proof" %
(bp["built"], bp["total"], (", %d partial" % bp["partial"]) if bp["partial"] else "", pct(bp["built"], bp["total"]), pct(bp["partial"], bp["total"])))
w("built %dpartial %dnot built %d%s" % (bp["built"], bp["partial"], bp["todo"], ("not needed %d (not counted)" % bp["skip"]) if bp["skip"] else ""))
w("" + "".join("" % (k, v, pct(v, bp["total"]), KC.get(k, "#888")) for k, v in kinds.items()) + "")
@@ -1753,16 +1753,18 @@ def chip(f, cls=""):
w("")
for p in bp["phases"]:
now = p is cur
- w("Phase %s: %s%s%d of %d rows built%s%s%smodule files: %sClick to show the rows
" %
- (" now" if now else "", esc(p["id"]), esc(p["id"]), esc(p["name"]), " building now" if now else "", pct(p["built"], p["n"]), pct(p["partial"], p["n"]), p["built"], p["n"], (", %d partial" % p["partial"]) if p["partial"] else "", (" · %d runs" % runs_by.get(str(p["id"]), 0)) if runs_by.get(str(p["id"])) else "", esc(" ".join("%s %d" % kv for kv in p["kinds"].items())),
+ w("Phase %s: %s%s%d of %d rows built%s%s%smodule files: %sClick to show or hide the rows
" %
+ (" now" if now else "", esc(p["id"]), " open" if now else "", esc(p["id"]), esc(p["name"]), " building now" if now else "", pct(p["built"], p["n"]), pct(p["partial"], p["n"]), p["built"], p["n"], (", %d partial" % p["partial"]) if p["partial"] else "", (" · %d runs" % runs_by.get(str(p["id"]), 0)) if runs_by.get(str(p["id"])) else "", esc(" ".join("%s %d" % kv for kv in p["kinds"].items())),
esc(" ".join(("%s %s" % (n, "yes" if v else "no")) for n, v in (("definition", p["def"]), ("brief", p["brief"]), ("ledger", p["ledger"]))))))
- w("# Kind Row State " + "".join("%s %s %s %s " % (x["cls"], esc(x["n"]), esc(x["kind"]), esc(x["step"]), esc(x["state"][:90])) for x in p["steps"]) + "
")
+ if str(p["id"]) in phase_detail: w(phase_detail[str(p["id"])])
+ else: w("# Kind Row State " + "".join("%s %s %s %s " % (x["cls"], esc(x["n"]), esc(x["kind"]), esc(x["step"]), esc(x["state"][:90])) for x in p["steps"]) + "
")
+ w("")
w("")
else: w("No plan to summarise yet: %s" % esc(args.plan_msg or "architecture/build-plan.md was not read."))
- if args.fragment: # the full plan tables repeat the phase cards above row by row, so they start folded
- w("Full plan tables and views A and BEvery row of build-plan.md with its claims and drift, the script folders (A) and module proof (B). The phase cards above already show each row.
")
+ if args.fragment: # plan tally, views A (script folders) and B (module proof); the per-phase rows live in the cards above
+ w("")
w(args.fragment)
- w(" ")
+ w("")
w(render_pages(proj, args.outdir, [SELF_OUT]))
# footer
w("