diff --git a/VERSION b/VERSION
index ace4423..42cf067 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.15.1
+1.15.2
diff --git a/VERSIONS.md b/VERSIONS.md
index 45ca9b2..470fe10 100644
--- a/VERSIONS.md
+++ b/VERSIONS.md
@@ -17,6 +17,7 @@ Git tags: `vMAJOR.FEATURE.FIX` (e.g. `v1.8.0`). File `VERSION` omits the `v` pre
| Tag | Date (UTC) | Notes |
|-----|------------|--------|
+| **v1.15.2** | 2026-07-19 | **Bugfix:** warn/error filter — drop blank log lines from HTML; compact console (no black voids above/mid/below); context skips SUMMARY chrome & pure box frames; tighter filter-gap. |
| **v1.15.1** | 2026-07-19 | **Bugfix:** mon HTML — phase `section()` boxes no longer painted as SUMMARY (were blue/green `sum-header`); only real SUMMARY rows get solid severity backgrounds (OK/WARN/ERROR/INFO). |
| **v1.15.0** | 2026-07-19 | **Feature:** CHF **fake-incoming amount ladder** in `devtesting` (`DEVTESTING_LADDER=1`, default **23** log-spaced rungs **CHF:0.01 → CHF:50000**); synthetic geniban debit funds each rung; baseline plumbing probe kept. |
| **v1.14.0** | 2026-07-19 | **Feature:** SUMMARY box rows are **colour-distinct** (per-severity badge + tinted background + bold counts; verdict-coloured header); HTML mon pages style `sum-ok` / `sum-warn` / `sum-err` / … panels the same way. |
diff --git a/site-gen/console_to_html.py b/site-gen/console_to_html.py
index 43ffcd2..89dd4ee 100755
--- a/site-gen/console_to_html.py
+++ b/site-gen/console_to_html.py
@@ -549,6 +549,10 @@ def slug_error(i: int, text: str) -> str:
def render_line(ln: str, err_slugs: dict[str, str], extra_ids: list[str] | None = None) -> str:
+ # Drop blank log lines entirely — they only create black voids in the console
+ # (especially visible under warn/error filter). Anchors still attach to next real line.
+ if not (ln or "").strip():
+ return ""
kind = classify(ln)
esc = html.escape(ln)
for tid, slug in err_slugs.items():
@@ -563,8 +567,11 @@ def render_line(ln: str, err_slugs: dict[str, str], extra_ids: list[str] | None
# HTML allows one id — join carefully
id_attr = f' id="{html.escape(extra_ids[0])}"'
if len(extra_ids) > 1:
- # secondary anchors as empty spans prepended
- spans = "".join(f'' for x in extra_ids[1:])
+ # secondary anchors as empty spans prepended (inline, zero layout impact)
+ spans = "".join(
+ f''
+ for x in extra_ids[1:]
+ )
return f'{spans}
{esc}
\n'
return f'{esc}
\n'
@@ -1175,38 +1182,83 @@ body.filter-warn .filter-chip { border-color: #a68b2d; background: #1a1608; colo
padding: 1px 8px;
}
.filter-clear:hover { border-color: #aaa; background: #222; }
-/* Console filter: sticky + overviews stay; match lines + dimmed neighbours (v1.13.11) */
-body.filter-error #mon-console .line:not(.error):not(.blocker):not(.filter-ctx) { display: none !important; }
-body.filter-warn #mon-console .line:not(.warn):not(.filter-ctx) { display: none !important; }
+/* Console filter: sticky + overviews stay; match lines + dimmed neighbours (v1.13.11+)
+ v1.15.2: stamp out black voids — hide blanks, zero-size non-matches, compact console */
+body.filter-error #mon-console,
+body.filter-warn #mon-console {
+ padding: 6px 8px;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+}
+body.filter-error #mon-console .line:not(.error):not(.blocker):not(.filter-ctx),
+body.filter-warn #mon-console .line:not(.warn):not(.filter-ctx) {
+ display: none !important;
+ height: 0 !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ border: none !important;
+ overflow: hidden !important;
+ line-height: 0 !important;
+}
+/* empty / whitespace-only lines never take space in filter mode */
+body.filter-error #mon-console .line:empty,
+body.filter-warn #mon-console .line:empty,
+body.filter-error #mon-console .line.line-blank,
+body.filter-warn #mon-console .line.line-blank {
+ display: none !important;
+}
/* Context lines: grayed / muted so the warn|error still stands out */
body.filter-error #mon-console .line.filter-ctx,
body.filter-warn #mon-console .line.filter-ctx {
- opacity: 0.48;
- color: #7a7a7a !important;
- filter: grayscale(0.9);
+ opacity: 0.55;
+ color: #8a8a8a !important;
+ filter: grayscale(0.85);
+ margin: 0 !important;
+ padding: 1px 4px !important;
+ border: none !important;
+ background: transparent !important;
}
body.filter-error #mon-console .line.filter-ctx a.jump,
body.filter-warn #mon-console .line.filter-ctx a.jump {
color: #6a6a6a !important;
}
-/* Visual break when filter jumps across a gap in the log */
+/* Match lines stay tight */
+body.filter-error #mon-console .line.error,
+body.filter-error #mon-console .line.blocker,
+body.filter-warn #mon-console .line.warn {
+ margin: 1px 0 !important;
+ padding: 3px 6px !important;
+}
+/* Compact ellipsis between distant clusters (not a black slab) */
body.filter-error #mon-console .filter-gap,
body.filter-warn #mon-console .filter-gap {
display: block;
- color: #555;
+ color: #666;
text-align: center;
- padding: 6px 0;
- margin: 4px 0;
- letter-spacing: 0.35em;
- font-size: 11px;
+ padding: 0;
+ margin: 2px 0;
+ letter-spacing: 0.25em;
+ font-size: 10px;
+ line-height: 1.2;
user-select: none;
- border-top: 1px dashed #333;
- border-bottom: 1px dashed #333;
+ border: none;
+ background: transparent;
+ height: auto;
}
body:not(.filter-error):not(.filter-warn) #mon-console .filter-gap { display: none !important; }
body.filter-error .env-context,
body.filter-warn .env-context { display: none !important; }
body.filter-warn .err-top { display: none; }
+/* secondary jump anchors: never affect layout */
+#mon-console .anchor-only {
+ display: inline;
+ width: 0;
+ height: 0;
+ overflow: hidden;
+ position: absolute;
+}
/* v1.12.0: collapsible monitoring env context (default closed) */
.env-context {
margin: 0 0 12px;
@@ -1436,14 +1488,14 @@ STICKY_JS = """
}
// v1.11.0: click error/warn stats → filter log; sticky + overviews stay
- // v1.13.11: keep a few neighbour lines (dimmed) so phase/group context stays readable
+ // v1.13.11: dimmed neighbour context; v1.15.2: stamp out black voids, compact layout
var bar = document.getElementById("status-bar");
var chip = document.getElementById("filter-chip");
var chipLabel = document.getElementById("filter-chip-label");
var clearBtn = document.getElementById("filter-clear");
var consoleEl = document.getElementById("mon-console");
- // lines above + below each match (same count either side)
- var FILTER_CTX = 3;
+ // useful neighbours each side (skip blanks / SUMMARY chrome / pure box frames)
+ var FILTER_CTX = 2;
function currentFilter() {
if (document.body.classList.contains("filter-error")) return "error";
@@ -1459,11 +1511,13 @@ STICKY_JS = """
consoleEl.querySelectorAll(".filter-gap").forEach(function (el) {
el.parentNode && el.parentNode.removeChild(el);
});
+ document.body.classList.remove("filter-compact");
}
function markFilterContext(mode) {
clearFilterContext();
if (!consoleEl || !mode) return;
+ document.body.classList.add("filter-compact");
var lines = Array.prototype.slice.call(consoleEl.querySelectorAll(".line"));
if (!lines.length) return;
function isMatch(el) {
@@ -1477,19 +1531,30 @@ STICKY_JS = """
var t = (el.textContent || "").replace(/\u00a0/g, " ").trim();
return t.length === 0;
}
- // 0 = hide, 1 = match, 2 = context
+ function isLowValue(el) {
+ if (isBlankLine(el)) return true;
+ if (el.classList.contains("sum-header") || el.classList.contains("sum-ok")
+ || el.classList.contains("sum-warn") || el.classList.contains("sum-err")
+ || el.classList.contains("sum-info") || el.classList.contains("sum-block")
+ || el.classList.contains("sum-totals") || el.classList.contains("sum-verdict-ok")
+ || el.classList.contains("sum-verdict-warn") || el.classList.contains("sum-verdict-err")) {
+ return true;
+ }
+ var t = (el.textContent || "").replace(/\u00a0/g, " ").trim();
+ if (/^[╔╗╚╝║═\-\s│┌┐└┘]+$/.test(t)) return true;
+ return false;
+ }
var keep = new Array(lines.length);
var i, j, k, n, idx;
for (i = 0; i < lines.length; i++) keep[i] = 0;
for (i = 0; i < lines.length; i++) {
if (!isMatch(lines[i])) continue;
keep[i] = 1;
- // ±FILTER_CTX *non-empty* neighbours (skip blank lines so filter is not full of voids)
n = 0;
idx = i - 1;
while (idx >= 0 && n < FILTER_CTX) {
if (keep[idx] === 1) { idx--; continue; }
- if (isBlankLine(lines[idx])) { idx--; continue; }
+ if (isLowValue(lines[idx])) { idx--; continue; }
keep[idx] = 2;
n++;
idx--;
@@ -1498,7 +1563,7 @@ STICKY_JS = """
idx = i + 1;
while (idx < lines.length && n < FILTER_CTX) {
if (keep[idx] === 1) { idx++; continue; }
- if (isBlankLine(lines[idx])) { idx++; continue; }
+ if (isLowValue(lines[idx])) { idx++; continue; }
keep[idx] = 2;
n++;
idx++;
@@ -1506,10 +1571,11 @@ STICKY_JS = """
}
for (j = 0; j < lines.length; j++) {
if (keep[j] === 2) lines[j].classList.add("filter-ctx");
- // never show pure blank lines in filter mode
- if (isBlankLine(lines[j]) && keep[j] !== 1) keep[j] = 0;
+ if (isBlankLine(lines[j])) {
+ keep[j] = 0;
+ lines[j].classList.add("line-blank");
+ }
}
- // dashed gap between non-contiguous visible clusters
var prevVisible = -2;
for (k = 0; k < lines.length; k++) {
if (keep[k] === 0) continue;