feat: v1.1.0 redesign — light default, real toggle, kanban board, sticky nav

- Theme is now a manual toggle (real sun/moon SVG icons, not glyphs that
  don't render consistently), persisted to localStorage, defaults to light
  on first visit. No more silent OS-based auto-switching.
- Small wordmark + mark in a thin top bar so the page is recognizable as
  sitrep-panel, not just the task title.
- Status board rewritten as four kanban-style columns (to do / in progress
  / done / blocked) with live counts, instead of a loose wrap of
  identically-styled cards.
- Every section gets a one-line description of its purpose.
- Sticky section nav with scroll-spy highlighting — real wayfinding without
  hiding any content behind a click, since the whole point is to be
  glanceable.
- Log entries use a proper timeline rail (dot + connecting line) instead of
  a colored border-left accent.
- Palette rebuilt in OKLCH with neutrals tinted toward the accent hue.

Updates the SKILL.md/AGENTS.md protocol and validator for the new
column-based board markup and entry structure.
This commit is contained in:
dbl8005
2026-08-23 00:47:27 +03:00
parent f4d9a01e9e
commit 3e4d4c46da
8 changed files with 308 additions and 115 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "sitrep-panel",
"description": "A live local HTML report Claude keeps updated as it works — status board, running narrative, screenshots, newest-first log — so you can watch progress without reading the transcript.",
"version": "1.0.0",
"version": "1.1.0",
"author": {
"name": "Dave"
},
+13 -7
View File
@@ -81,16 +81,22 @@ Update it at every real step:
what you're doing right now and *why*. Replace the block's content each
time; it always reflects the present, not history.
2. **Prepend a new log entry** to `#entries` (newest-first — insert right
after the `<div id="entries">` opening tag). One entry per real step:
what happened, what was decided and why, what was verified. Skip entries
for trivial reads; log entries for writes, decisions, and verification.
after the `<div id="entries">` opening tag), using the exact `.entry` /
`.entry-rail` / `.entry-dot` markup shown in the template's commented
example — the timeline rail depends on that structure. One entry per real
step: what happened, what was decided and why, what was verified. Skip
entries for trivial reads; log entries for writes, decisions, and
verification.
3. **Save screenshots** into `.sitrep-panel/report/screenshots/` and
reference them with a relative `<img src="screenshots/whatever.png">` —
inline in a log entry, appended to `#shots-body`, or both.
4. **Update the board** (`#board`) — add/move/remove `.board-card` elements
as steps start, block, or finish. See the commented example markup in
the template for the exact class names (`status-todo` /
`status-progress` / `status-done` / `status-blocked`).
4. **Update the board** — the board is four columns, each with its own
container: `#board-todo`, `#board-progress`, `#board-done`,
`#board-blocked`. Add/move/remove a plain `<div class="board-card">…</div>`
in the container matching its current status as steps start, block, or
finish — moving a card means removing it from one column's container and
appending it to another's, not changing a class. See the commented
example markup in the template.
5. **Touch `meta.json` last, always.** Rewrite `updated_at` to the current
ISO-8601 UTC timestamp as the final write of every update — the page
polls this file every 2s and reloads on change. This is the one step
+8 -4
View File
@@ -30,10 +30,10 @@ everything is one static HTML page plus a stdlib Python server.
## What it shows
- **Status board** — cards for what's to do, in progress, done, or blocked.
Synced from a connected issue tracker (Jira, GitHub Issues, Linear —
whatever the agent already has access to) when one's available, otherwise
a plain checklist the agent maintains by hand. Never fabricated.
- **Status board** — four columns: to do, in progress, done, blocked. Synced
from a connected issue tracker (Jira, GitHub Issues, Linear — whatever the
agent already has access to) when one's available, otherwise a plain
checklist the agent maintains by hand. Never fabricated.
- **What's happening now** — a standing paragraph of *why*, not a status
word. Replaced each update, always reflecting the present.
- **Screenshots** — dropped in as they're taken, inline in the log or in
@@ -102,6 +102,10 @@ package files; CI runs it plus a real server-start smoke test on every PR.
## Versions
- **1.1.0** — redesign: light mode by default with a real theme toggle (no
more silent OS-based switching), a small wordmark, the status board
regrouped into kanban-style columns instead of a loose card grid, and a
sticky section nav for wayfinding without hiding anything.
- **1.0.0** — initial release: status board, live "what's happening now"
narrative, screenshot gallery, newest-first log, live-reload, optional
tracker-synced board.
+14 -9
View File
@@ -9,7 +9,7 @@ description: |
or wants visibility into a multi-step task as it happens.
license: MIT
metadata:
version: "1.0.0"
version: "1.1.0"
---
# sitrep-panel
@@ -100,17 +100,22 @@ the start is worthless. Update it at every real step, not just at the end:
present, not history.
2. **Prepend a new log entry** to `#entries` (newest-first — insert right
after the `<div id="entries">` opening tag, before whatever was already
there). One entry per real step: what happened, what you decided and why,
what you verified. Skip entries for trivial reads; log entries for
writes, decisions, and verification results.
there), using the exact `.entry` / `.entry-rail` / `.entry-dot` markup
shown in the template's commented example — the timeline rail depends on
that structure. One entry per real step: what happened, what you decided
and why, what you verified. Skip entries for trivial reads; log entries
for writes, decisions, and verification results.
3. **Save screenshots** into `.sitrep-panel/report/screenshots/` and
reference them with a relative `<img src="screenshots/whatever.png">` —
either inline in a log entry, or appended to `#shots-body` (or both, when
a screenshot is the standout evidence for a step).
4. **Update the board** (`#board`) — add/move/remove `.board-card` elements
as steps start, block, or finish. See the commented example markup
already in the template for the exact class names
(`status-todo` / `status-progress` / `status-done` / `status-blocked`).
4. **Update the board** — the board is four columns, each with its own
container: `#board-todo`, `#board-progress`, `#board-done`,
`#board-blocked`. Add/move/remove a plain `<div class="board-card">…</div>`
in the container matching its current status as steps start, block, or
finish — moving a card means removing it from one column's container and
appending it to another's, not changing a class. See the commented
example markup in the template.
5. **Touch `meta.json` last, always.** Rewrite `updated_at` to the current
ISO-8601 UTC timestamp as the final write of every update — the page
polls this file every 2s and reloads on change, so writing it before the
@@ -123,7 +128,7 @@ If the project has a working issue tracker connection available to you right
now (an already-connected Atlassian/Jira MCP, `gh issue list` for a GitHub
repo, a connected Linear MCP, etc.) **and** there are real tickets relevant
to this task, render the board from that real data — real ticket key, real
title, real link, real status mapped to the four status classes. Refresh it
title, real link, real status mapped to the matching column. Refresh it
each time you update the doc, the same as everything else.
If no tracker is connected, or nothing relevant is filed there, fall back to
+266 -93
View File
@@ -3,49 +3,85 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='7' fill='%232f6fed'/%3E%3C/svg%3E">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='2' fill='%232f6fed'/%3E%3Cpath d='M4 8a4 4 0 0 1 8 0' stroke='%232f6fed' stroke-width='1.3' stroke-linecap='round' opacity='.55' fill='none'/%3E%3Cpath d='M1.5 8a6.5 6.5 0 0 1 13 0' stroke='%232f6fed' stroke-width='1.3' stroke-linecap='round' opacity='.3' fill='none'/%3E%3C/svg%3E">
<title>{{TITLE}} — sitrep-panel</title>
<style>
:root {
--bg: #f7f7f8;
--bg-raised: #ffffff;
--border: #e2e2e6;
--text: #1a1a1f;
--text-muted: #6b6b76;
--accent: #2f6fed;
--status-todo-bg: #eceef1; --status-todo-fg: #52525b;
--status-progress-bg: #fff4d6; --status-progress-fg: #92650a;
--status-done-bg: #dcf5e3; --status-done-fg: #146c2e;
--status-blocked-bg: #fde2e1; --status-blocked-fg: #a3221c;
--code-bg: #f0f0f3;
--hue: 250;
--bg: oklch(98% 0.003 var(--hue));
--bg-raised: oklch(99.4% 0.002 var(--hue));
--border: oklch(90% 0.006 var(--hue));
--text: oklch(22% 0.01 var(--hue));
--text-muted: oklch(48% 0.01 var(--hue));
--accent: oklch(58% 0.19 258);
--accent-contrast: oklch(99% 0.005 258);
--code-bg: oklch(95% 0.004 var(--hue));
--status-todo-bg: oklch(94% 0.005 var(--hue)); --status-todo-fg: oklch(45% 0.01 var(--hue));
--status-progress-bg: oklch(93% 0.06 85); --status-progress-fg: oklch(46% 0.13 75);
--status-done-bg: oklch(93% 0.06 155); --status-done-fg: oklch(42% 0.13 155);
--status-blocked-bg: oklch(93% 0.06 25); --status-blocked-fg: oklch(48% 0.17 25);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0b0d12;
--bg-raised: #14161d;
--border: #262933;
--text: #e8e9ed;
--text-muted: #8b8d98;
--accent: #6c9bff;
--status-todo-bg: #23252c; --status-todo-fg: #a6a8b3;
--status-progress-bg: #3a2f0f; --status-progress-fg: #f0c04a;
--status-done-bg: #12301d; --status-done-fg: #4ad980;
--status-blocked-bg: #3a1414; --status-blocked-fg: #ff6a63;
--code-bg: #1b1d24;
}
:root[data-theme="dark"] {
--bg: oklch(16% 0.008 var(--hue));
--bg-raised: oklch(21% 0.01 var(--hue));
--border: oklch(31% 0.015 var(--hue));
--text: oklch(93% 0.004 var(--hue));
--text-muted: oklch(66% 0.012 var(--hue));
--accent: oklch(74% 0.15 258);
--accent-contrast: oklch(18% 0.02 258);
--code-bg: oklch(24% 0.012 var(--hue));
--status-todo-bg: oklch(27% 0.01 var(--hue)); --status-todo-fg: oklch(72% 0.012 var(--hue));
--status-progress-bg: oklch(30% 0.06 85); --status-progress-fg: oklch(80% 0.11 85);
--status-done-bg: oklch(28% 0.055 155); --status-done-fg: oklch(75% 0.12 155);
--status-blocked-bg: oklch(30% 0.08 25); --status-blocked-fg: oklch(78% 0.14 25);
}
* { box-sizing: border-box; }
html { color-scheme: light; }
html[data-theme="dark"] { color-scheme: dark; }
body {
margin: 0;
padding: 2rem 1.25rem 4rem;
background: var(--bg);
color: var(--text);
font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
}
main { max-width: 860px; margin: 0 auto; }
header { margin-bottom: 1.75rem; }
h1 { font-size: 1.5rem; margin: 0 0 0.25rem; }
.sub { color: var(--text-muted); margin: 0 0 0.5rem; font-size: 0.9rem; }
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.6rem 1.25rem;
border-bottom: 1px solid var(--border);
}
.brand {
display: flex;
align-items: center;
gap: 0.4rem;
color: var(--text-muted);
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.02em;
}
.brand svg { color: var(--accent); }
#theme-toggle {
appearance: none;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border);
background: var(--bg-raised);
color: var(--text-muted);
border-radius: 6px;
width: 30px;
height: 30px;
cursor: pointer;
}
#theme-toggle:hover { color: var(--text); border-color: var(--text-muted); }
#theme-toggle svg { display: block; }
main, .task-header, .section-nav { max-width: 860px; margin: 0 auto; padding-left: 1.25rem; padding-right: 1.25rem; }
.task-header { padding-top: 1.75rem; padding-bottom: 1rem; }
h1 { font-size: 1.6rem; margin: 0 0 0.3rem; letter-spacing: -0.01em; }
.sub { color: var(--text-muted); margin: 0 0 0.6rem; font-size: 0.92rem; max-width: 70ch; }
.updated-badge {
display: inline-block;
font-size: 0.75rem;
@@ -55,43 +91,79 @@
border-radius: 999px;
padding: 0.2rem 0.65rem;
}
section { margin-bottom: 2rem; }
h2 {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
margin: 0 0 0.75rem;
font-weight: 600;
}
h2 .hint { text-transform: none; letter-spacing: normal; font-weight: 400; }
.board { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.board-card {
.section-nav {
position: sticky;
top: 0;
z-index: 5;
display: flex;
gap: 1.25rem;
background: var(--bg);
border-bottom: 1px solid var(--border);
padding-top: 0.6rem;
padding-bottom: 0.6rem;
}
.section-nav a {
color: var(--text-muted);
text-decoration: none;
font-size: 0.85rem;
font-weight: 500;
padding-bottom: 0.5rem;
border-bottom: 2px solid transparent;
}
.section-nav a.active { color: var(--text); border-bottom-color: var(--accent); }
main { padding-top: 1.75rem; padding-bottom: 4rem; }
section { margin-bottom: 2.5rem; scroll-margin-top: 3.5rem; }
h2 { font-size: 1.05rem; margin: 0 0 0.2rem; font-weight: 650; }
h2 .hint { font-weight: 400; color: var(--text-muted); font-size: 0.85rem; }
.section-hint { color: var(--text-muted); font-size: 0.85rem; margin: 0 0 1rem; }
.board { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.75rem; }
.board-col-head {
display: flex;
align-items: center;
gap: 0.5rem;
justify-content: space-between;
font-size: 0.72rem;
font-weight: 650;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-muted);
padding-bottom: 0.5rem;
margin-bottom: 0.5rem;
border-bottom: 1px solid var(--border);
}
.board-col[data-status="progress"] .board-col-head { color: var(--status-progress-fg); }
.board-col[data-status="done"] .board-col-head { color: var(--status-done-fg); }
.board-col[data-status="blocked"] .board-col-head { color: var(--status-blocked-fg); }
.board-col-head .count {
font-weight: 600;
background: var(--bg-raised);
border: 1px solid var(--border);
border-radius: 999px;
padding: 0 0.4rem;
font-size: 0.7rem;
}
.board-col-body { display: flex; flex-direction: column; gap: 0.4rem; min-height: 1.5rem; }
.board-card {
background: var(--bg-raised);
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.45rem 0.75rem;
font-size: 0.85rem;
padding: 0.5rem 0.65rem;
font-size: 0.83rem;
line-height: 1.35;
}
.card-status {
font-size: 0.68rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
padding: 0.1rem 0.45rem;
border-radius: 999px;
white-space: nowrap;
.board-card a { color: inherit; text-decoration: none; }
.board-card a:hover { text-decoration: underline; }
.board-col-body:empty::after {
content: "—";
color: var(--text-muted);
font-size: 0.8rem;
opacity: 0.5;
}
@media (max-width: 720px) {
.board { grid-template-columns: repeat(2, 1fr); }
}
.status-todo .card-status { background: var(--status-todo-bg); color: var(--status-todo-fg); }
.status-progress .card-status { background: var(--status-progress-bg); color: var(--status-progress-fg); }
.status-done .card-status { background: var(--status-done-bg); color: var(--status-done-fg); }
.status-blocked .card-status { background: var(--status-blocked-bg); color: var(--status-blocked-fg); }
.card-title a { color: inherit; text-decoration: none; }
.card-title a:hover { text-decoration: underline; }
.current-work {
background: var(--bg-raised);
@@ -99,8 +171,9 @@
border-radius: 10px;
padding: 1rem 1.25rem;
}
.current-work p { margin: 0 0 0.75rem; }
.current-work p:last-child { margin-bottom: 0; }
.current-work h2, .current-work .section-hint { padding: 0 0 0 0; }
.current-work p { margin: 0.75rem 0 0; max-width: 75ch; }
.current-work p:first-of-type { margin-top: 0; }
.current-work .placeholder { color: var(--text-muted); font-style: italic; }
.current-work code, .entry code {
background: var(--code-bg);
@@ -125,25 +198,29 @@
font-size: 0.85rem;
}
.entry {
border-left: 2px solid var(--border);
padding: 0 0 0 1rem;
margin-bottom: 1.25rem;
position: relative;
}
.entry::before {
.entry { display: grid; grid-template-columns: 20px 1fr; column-gap: 0.75rem; padding-bottom: 1.25rem; }
.entry-rail { position: relative; }
.entry-rail::before {
content: "";
position: absolute;
left: -5px;
top: 0.35rem;
width: 8px;
height: 8px;
left: 50%;
top: 1.1rem;
bottom: -1.25rem;
width: 1px;
background: var(--border);
transform: translateX(-50%);
}
.entry:last-child .entry-rail::before { display: none; }
.entry-dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--accent);
margin-top: 0.4rem;
}
.entry-time { font-size: 0.75rem; color: var(--text-muted); margin-bottom: 0.15rem; }
.entry-title { font-weight: 600; margin-bottom: 0.35rem; }
.entry-body p { margin: 0 0 0.5rem; }
.entry-body p { margin: 0 0 0.5rem; max-width: 75ch; }
.entry-body p:last-child { margin-bottom: 0; }
.entry-body img {
max-width: 100%;
@@ -159,48 +236,88 @@
</style>
</head>
<body>
<main>
<header>
<div class="topbar">
<div class="brand">
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<circle cx="8" cy="8" r="2" fill="currentColor"/>
<path d="M4 8a4 4 0 0 1 8 0" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" opacity="0.55"/>
<path d="M1.5 8a6.5 6.5 0 0 1 13 0" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" opacity="0.3"/>
</svg>
sitrep-panel
</div>
<button id="theme-toggle" type="button" aria-label="Switch to dark"></button>
</div>
<header class="task-header">
<h1>{{TITLE}}</h1>
<p class="sub">{{SUBTITLE}}</p>
<span class="updated-badge" id="updated-badge">just started</span>
</header>
<section class="board" id="board">
<!-- board-card elements go here. Manual example:
<div class="board-card status-progress">
<span class="card-status">In progress</span>
<span class="card-title">Step name</span>
<nav class="section-nav" id="section-nav">
<a href="#board-section" data-target="board-section">Board</a>
<a href="#now-section" data-target="now-section">Now</a>
<a href="#shots-section" data-target="shots-section">Screenshots</a>
<a href="#log-section" data-target="log-section" id="nav-log">Log</a>
</nav>
<main>
<section class="board-section" id="board-section">
<h2>Status board</h2>
<p class="section-hint">Where each part of the task stands right now.</p>
<div class="board" id="board">
<div class="board-col" data-status="todo">
<div class="board-col-head">To do <span class="count" id="count-todo">0</span></div>
<div class="board-col-body" id="board-todo"></div>
</div>
<div class="board-col" data-status="progress">
<div class="board-col-head">In progress <span class="count" id="count-progress">0</span></div>
<div class="board-col-body" id="board-progress"></div>
</div>
<div class="board-col" data-status="done">
<div class="board-col-head">Done <span class="count" id="count-done">0</span></div>
<div class="board-col-body" id="board-done"></div>
</div>
<div class="board-col" data-status="blocked">
<div class="board-col-head">Blocked <span class="count" id="count-blocked">0</span></div>
<div class="board-col-body" id="board-blocked"></div>
</div>
</div>
<!-- Card markup, placed in the matching board-todo / board-progress / board-done /
board-blocked container:
<div class="board-card">Step name</div>
Tracker-synced example (real ticket key + link, never fabricated):
<div class="board-card status-todo">
<span class="card-status">To do</span>
<span class="card-title"><a href="https://...">PROJ-123 issue title</a></span>
</div>
<div class="board-card"><a href="https://...">PROJ-123 issue title</a></div>
-->
</section>
<section class="current-work">
<section class="current-work" id="now-section">
<h2>What's happening now</h2>
<p class="section-hint">What's actually happening, in plain language — not just a status word.</p>
<div id="current-work-body">
<p class="placeholder">Nothing yet — this fills in once work starts.</p>
</div>
</section>
<section class="shots">
<section class="shots" id="shots-section">
<h2>Screenshots</h2>
<p class="section-hint">Visual proof, dropped in as it happens.</p>
<div class="shots-body" id="shots-body"></div>
</section>
<section class="log">
<section class="log" id="log-section">
<h2>Log <span class="hint">(newest first)</span></h2>
<p class="section-hint">Everything that's happened, in order.</p>
<div id="entries">
<!-- New entries are PREPENDED here, right after this comment. Example:
<div class="entry" data-ts="2026-01-01T00:00:00Z">
<div class="entry-time">Jan 1, 00:00</div>
<div class="entry-title">Short title of what happened</div>
<div class="entry-body">
<p>Prose describing what happened and why.</p>
<div class="entry">
<div class="entry-rail"><div class="entry-dot"></div></div>
<div>
<div class="entry-time">Jan 1, 00:00</div>
<div class="entry-title">Short title of what happened</div>
<div class="entry-body">
<p>Prose describing what happened and why.</p>
</div>
</div>
</div>
-->
@@ -210,6 +327,62 @@
<script>
(function () {
// Theme: manual toggle only, defaults to light. No OS auto-detection —
// this tool is watched in a normal browser tab, not a dim ops room.
var KEY = 'sitrep-panel-theme';
var root = document.documentElement;
var btn = document.getElementById('theme-toggle');
// Real icons, not emoji/dingbat glyphs — glyph support for things like
// ☽/☀ varies enough across fonts/platforms that it can render as tofu.
var SUN = '<svg width="15" height="15" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="3.2" stroke="currentColor" stroke-width="1.3"/><g stroke="currentColor" stroke-width="1.3" stroke-linecap="round"><path d="M8 1v1.5M8 13.5V15M15 8h-1.5M2.5 8H1M12.7 3.3l-1 1M4.3 11.7l-1 1M12.7 12.7l-1-1M4.3 4.3l-1-1"/></g></svg>';
var MOON = '<svg width="15" height="15" viewBox="0 0 16 16" fill="none"><path d="M13.5 9.7A5.8 5.8 0 1 1 6.3 2.5a4.6 4.6 0 0 0 7.2 7.2z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>';
function apply(theme) {
if (theme === 'dark') root.setAttribute('data-theme', 'dark');
else root.removeAttribute('data-theme');
btn.innerHTML = theme === 'dark' ? SUN : MOON;
btn.setAttribute('aria-label', theme === 'dark' ? 'Switch to light' : 'Switch to dark');
}
var saved = null;
try { saved = localStorage.getItem(KEY); } catch (e) { /* private mode etc. */ }
apply(saved === 'dark' ? 'dark' : 'light');
btn.addEventListener('click', function () {
var next = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
apply(next);
try { localStorage.setItem(KEY, next); } catch (e) { /* ignore */ }
});
})();
(function () {
// Section nav: highlights whichever section is in view. Nothing is
// hidden — this is wayfinding, not tabs.
var links = Array.prototype.slice.call(document.querySelectorAll('#section-nav a'));
var sections = links.map(function (a) { return document.getElementById(a.dataset.target); }).filter(Boolean);
if (!sections.length || typeof IntersectionObserver === 'undefined') return;
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) return;
links.forEach(function (a) { a.classList.toggle('active', a.dataset.target === entry.target.id); });
});
}, { rootMargin: '-45% 0px -50% 0px', threshold: 0 });
sections.forEach(function (s) { observer.observe(s); });
})();
(function () {
// Counts: recomputed on every load (a live-reload IS a fresh load).
function count(id) { var el = document.getElementById(id); return el ? el.children.length : 0; }
['todo', 'progress', 'done', 'blocked'].forEach(function (status) {
var el = document.getElementById('count-' + status);
if (el) el.textContent = count('board-' + status);
});
var navLog = document.getElementById('nav-log');
if (navLog) navLog.textContent = 'Log (' + count('entries') + ')';
})();
(function () {
// Live-reload: poll meta.json every 2s, reload the instant it changes.
var badge = document.getElementById('updated-badge');
var known = null;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 220 KiB

+6 -1
View File
@@ -74,11 +74,16 @@ require(
require(len(AGENTS.strip()) > 200, "AGENTS.md looks empty or too short")
# --- template.html has the ids the SKILL.md protocol depends on ------------
for element_id in ("board", "current-work-body", "shots-body", "entries", "updated-badge"):
for element_id in (
"board", "board-todo", "board-progress", "board-done", "board-blocked",
"current-work-body", "shots-body", "entries", "updated-badge",
"theme-toggle", "section-nav",
):
require(f'id="{element_id}"' in TEMPLATE, f'template.html is missing id="{element_id}"')
for token in ("{{TITLE}}", "{{SUBTITLE}}"):
require(token in TEMPLATE, f"template.html is missing the {token} placeholder")
require("meta.json" in TEMPLATE, "template.html must poll meta.json for live-reload")
require("data-theme" in TEMPLATE, "template.html must support a manual light/dark toggle")
# --- serve.py is at least syntactically valid Python ------------------------
try: