diff --git a/manager/core/board.html b/manager/core/board.html
index b2b367f..04c347f 100644
--- a/manager/core/board.html
+++ b/manager/core/board.html
@@ -236,7 +236,7 @@
}
#loghead .xfile{cursor:pointer;padding:1px 0;white-space:nowrap}
#loghead .xfile:hover{color:var(--accent)}
- #logbody{flex:1;min-height:0;overflow-y:auto;padding:6px 18px 12px;display:grid;gap:2px;align-content:start}
+ #logbody{flex:none;max-height:60vh;overflow-y:auto;padding:6px 18px 12px;display:grid;gap:2px;align-content:start}
.ev{display:grid;grid-template-columns:64px 18px 1fr;align-items:baseline;gap:10px;font-size:12px;min-width:0}
.ev time{font-family:var(--mono);font-size:11px;color:var(--dim)}
.ev .glyph{font-family:var(--mono);font-size:11.5px;color:var(--muted);text-align:center}
@@ -1804,8 +1804,10 @@ document.addEventListener('keydown', (e) => {
/* the activity log: drag its grip to resize, scroll to read back */
{
const grip = $('#loggrip'), body = $('#logbody');
- body.style.height = (parseInt(localStorage.getItem('bench-log-h'), 10) ||
- Math.round(innerHeight * 0.30)) + 'px';
+ const clamp = (px) => Math.min(Math.max(px, 80), Math.round(innerHeight * 0.6));
+ let h = clamp(parseInt(localStorage.getItem('bench-log-h'), 10) ||
+ Math.round(innerHeight * 0.30));
+ body.style.height = h + 'px';
let dragging = false, startY = 0, startH = 0;
grip.addEventListener('mousedown', (e) => {
dragging = true; startY = e.clientY; startH = body.offsetHeight;
@@ -1813,13 +1815,13 @@ document.addEventListener('keydown', (e) => {
});
document.addEventListener('mousemove', (e) => {
if (!dragging) return;
- const h = Math.min(Math.max(startH + (startY - e.clientY), 80), Math.round(innerHeight * 0.6));
+ h = clamp(startH + (startY - e.clientY));
body.style.height = h + 'px';
});
document.addEventListener('mouseup', () => {
if (!dragging) return;
dragging = false; grip.classList.remove('dragging');
- localStorage.setItem('bench-log-h', body.offsetHeight);
+ localStorage.setItem('bench-log-h', h);
});
body.addEventListener('scroll', () => {
S.logStick = body.scrollTop + body.clientHeight >= body.scrollHeight - 8;
diff --git a/tests/test_log_resize.py b/tests/test_log_resize.py
new file mode 100644
index 0000000..2c73b1a
--- /dev/null
+++ b/tests/test_log_resize.py
@@ -0,0 +1,103 @@
+"""The activity log's resize grip actually resizes (task 04).
+
+The grip's drag handlers wrote #logbody's inline height while the CSS said
+flex:1 — so the flex algorithm sized the element and the write was dead.
+Worse, mouseup persisted a re-read offsetHeight (the flex-computed value),
+silently overwriting the remembered size with the status quo.
+
+board.html is a single file with inline JS and no frontend test runner, so
+these are source-level invariants over the rule and the handler block: the
+ones that, if broken, would decouple the written height from the laid-out
+height again, or bring back the offsetHeight re-read that corrupted the
+stored size.
+
+ python3 -m unittest discover -s tests -v
+"""
+
+from __future__ import annotations
+
+import re
+import unittest
+from pathlib import Path
+
+BOARD = Path(__file__).resolve().parents[1] / "manager" / "core" / "board.html"
+
+
+class LogResizeTests(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.html = BOARD.read_text(encoding="utf-8")
+
+ m = re.search(r"#logbody\{([^}]*)\}", cls.html)
+ assert m, "board.html lost its #logbody rule"
+ cls.logbody = m.group(1).replace(" ", "")
+
+ # the whole activity-log drag block, from its banner comment to the
+ # drawer of the next top-level comment
+ m = re.search(
+ r"/\* the activity log:.*?\n\{(.*?)\n\}", cls.html, re.S)
+ assert m, "board.html lost the activity-log resize block"
+ cls.logjs = m.group(1)
+
+ m = re.search(r"/\* the drawer:.*?\n\{(.*?)\n\}", cls.html, re.S)
+ assert m, "board.html lost the drawer resize block"
+ cls.drawerjs = m.group(1)
+
+ # ── the height the handler writes is the height the layout uses ──
+
+ def test_logbody_height_is_authoritative(self):
+ """flex:none (0 0 auto): the inline height the drag handlers write
+ sizes the element. flex:1 (basis 0% + grow) is what made the write
+ dead — the flex algorithm never consulted the height property."""
+ self.assertIn("flex:none", self.logbody,
+ "#logbody must not be flex-sized; the grip writes "
+ "style.height and the layout must honour it")
+ self.assertNotIn("flex:1", self.logbody)
+
+ def test_logbody_growth_is_guarded(self):
+ """With height authoritative again, a huge saved value or a shrunk
+ window must not overflow: CSS max-height mirrors the drag clamp's
+ 60vh ceiling."""
+ self.assertIn("max-height:60vh", self.logbody)
+
+ def test_drag_clamp_bounds_survive(self):
+ """The advertised 80px–60vh drag bounds live in the handler block."""
+ self.assertRegex(self.logjs, r"Math\.max\(px,\s*80\)")
+ self.assertRegex(self.logjs, r"innerHeight\s*\*\s*0\.6\b")
+
+ # ── the saved size round-trips instead of being overwritten ──
+
+ def test_mouseup_persists_the_computed_height(self):
+ """mouseup must save the value the drag computed. Re-reading
+ offsetHeight is the old bug: while the write was dead it persisted
+ the flex-computed height, corrupting the stored size (856 → 156)."""
+ save = re.search(r"setItem\('bench-log-h',\s*([^)]*)\)", self.logjs)
+ self.assertIsNotNone(save, "the log block must persist bench-log-h")
+ self.assertNotIn("offsetHeight", save.group(1),
+ "persist the drag-computed value, not a re-read")
+
+ def test_restore_uses_the_drag_channel_and_clamp(self):
+ """The load-time restore writes the same property the drag writes,
+ through the same clamp — a saved value from a taller window must
+ not restore beyond today's 60vh."""
+ self.assertRegex(
+ self.logjs, r"clamp\([^)]*getItem\('bench-log-h'",
+ "the restore must clamp the saved height")
+ writes = re.findall(r"body\.style\.(\w+)\s*=", self.logjs)
+ self.assertTrue(writes, "the block must size #logbody")
+ self.assertEqual(set(writes), {"height"},
+ "restore and drag must write the same property")
+
+ # ── the drawer grip: same interaction family, must not regress ──
+
+ def test_drawer_width_is_still_authoritative(self):
+ """The drawer handlers write #drawer's own width; #drawer is
+ position:fixed, so no flex sizing competes with the write."""
+ drawer = re.search(r"#drawer\{([^}]*)\}", self.html).group(1)
+ self.assertIn("position:fixed", drawer.replace(" ", ""))
+ self.assertIn("panel.style.width", self.drawerjs)
+ self.assertIn("setItem('bench-drawer-w'", self.drawerjs)
+
+
+if __name__ == "__main__":
+ unittest.main()