feat: initial release of sitrep-panel v1.0.0

A live local HTML report skill for Claude Code — status board, running
'what's happening now' narrative, screenshot gallery, newest-first log,
served on localhost with 2s-poll live-reload (no build step, no external
requests). Optional tracker-synced board (Jira/GitHub Issues/Linear),
falls back to a manual checklist when nothing's configured.

Invoked via /sitrep-panel [start|open|stop|archive].
This commit is contained in:
dbl8005
2026-08-23 00:34:11 +03:00
commit f5702a8895
13 changed files with 842 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
{
"$schema": "https://json.schemastore.org/claude-code-marketplace-manifest.json",
"name": "sitrep-panel",
"owner": {
"name": "Dave"
},
"description": "Install sitrep-panel as a Claude Code plugin.",
"plugins": [
{
"name": "sitrep-panel",
"source": "./",
"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.",
"license": "MIT",
"keywords": ["progress", "report", "dashboard", "status", "live-reload", "session"]
}
]
}
+14
View File
@@ -0,0 +1,14 @@
{
"$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",
"author": {
"name": "Dave"
},
"homepage": "https://github.com/dbl8005/sitrep-panel",
"repository": "https://github.com/dbl8005/sitrep-panel",
"license": "MIT",
"keywords": ["progress", "report", "dashboard", "status", "live-reload", "session"],
"skills": ["./"]
}
+34
View File
@@ -0,0 +1,34 @@
name: Check package
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Check package files
run: python3 scripts/validate-package.py
- name: Check server script starts and serves
run: |
set -e
mkdir -p /tmp/pd-check/report
cp assets/template.html /tmp/pd-check/report/index.html
echo '{"updated_at": "2026-01-01T00:00:00Z"}' > /tmp/pd-check/report/meta.json
python3 scripts/serve.py /tmp/pd-check/report 8934 > /tmp/pd-serve.log 2>&1 &
server_pid=$!
sleep 1
url=$(grep -o 'http://localhost:[0-9]*/' /tmp/pd-serve.log | head -1)
test -n "$url"
status=$(curl -s -o /dev/null -w '%{http_code}' "${url}meta.json")
test "$status" = "200"
kill "$server_pid"
+3
View File
@@ -0,0 +1,3 @@
.DS_Store
__pycache__/
*.pyc
+120
View File
@@ -0,0 +1,120 @@
# sitrep-panel
A live local HTML report you keep updated as you work. It lives at
`.sitrep-panel/report/` inside whatever project you're invoked from, served
on localhost, and it live-reloads in the browser every time you update it —
the user never has to hit refresh.
This is not a hosted artifact. It never leaves the user's machine, needs no
publish step, and works offline. Use it whenever the user wants to *watch*
a task happen rather than read about it after the fact — they'll typically
ask by name ("start a sitrep-panel", "/sitrep-panel") or describe wanting a
"progress report", "status page", or "live doc" for a task.
## Invocation
`sitrep-panel [start | open | stop | archive]` — default (no argument) is
`start`.
- **`start`** (default) — scaffold a new report if none is active, or resume
the active one. Always ends by printing the localhost URL prominently.
- **`open`** — just print the URL of the active report (start the server if
it isn't running); never touches content.
- **`archive`** — move the current active report to
`.sitrep-panel/archive/<timestamp>-<slug>/` and scaffold a fresh one. Use
this when starting genuinely new, unrelated work rather than continuing
the current task.
- **`stop`** — kill the background server. Content on disk is untouched;
`start` or `open` later brings it back.
## First run — scaffold
If `.sitrep-panel/report/` doesn't exist yet in the current project:
1. Create `.sitrep-panel/report/screenshots/`.
2. Copy `assets/template.html` (from this package's own directory) to
`.sitrep-panel/report/index.html`. Replace `{{TITLE}}` with a short name
for the task, and `{{SUBTITLE}}` with one line of context (what this
report is tracking, and when it started).
3. Write `.sitrep-panel/report/meta.json`:
```json
{"updated_at": "<current ISO-8601 UTC timestamp>"}
```
4. If the project is a git repo and `.sitrep-panel/` is not already covered
by `.gitignore`, append it — this is local scratch, not something to
commit. Mention you did this; don't ask first, it's trivially reversible.
If `.sitrep-panel/report/` already exists, skip scaffolding — resume it. Use
`archive` first for unrelated new work if you want a clean slate.
## Serving it
Start the bundled server in the background, from this package's own
directory (so the relative path to `scripts/serve.py` resolves regardless of
the caller's cwd):
```bash
python3 <package-dir>/scripts/serve.py <project>/.sitrep-panel/report
```
Run this via your background-execution mechanism — it blocks forever. It
prints exactly one line, `SERVING http://localhost:<port>/`, before
blocking; capture that for the URL. It auto-picks a free port (tries 8934
first, falls back to any free port on conflict), so several reports can run
for different projects at once. Stdlib-only Python, no dependencies.
Record the port for reuse: write `.sitrep-panel/report/.server.json`
(`{"port": <port>, "started_at": "<iso8601>"}`), gitignored along with the
rest of `.sitrep-panel/`. On a later `start`/`open`, read this file first —
if a `GET` to `http://localhost:<port>/meta.json` succeeds, the server is
already up; reuse that URL instead of spawning a second one.
**Always tell the user the URL after `start` or `open`, even if you just
reused an existing server.**
## The update protocol — what makes this useful
A sitrep-panel that only gets written once at the start is worthless.
Update it at every real step:
1. **Update "What's happening now"** (`#current-work-body`) with prose —
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.
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`).
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
you must never skip.
## Tracker-synced board (optional)
If the project has a working issue-tracker connection available right now
and there are real tickets relevant to this task, render the board from
that real data — real key, real title, real link, real status. Refresh it
each time you update the doc.
If no tracker is connected, or nothing relevant is filed there, fall back
to a plain manually-maintained checklist kept in sync by hand. Never
fabricate ticket data, and don't silently guess which tracker to use if
more than one is plausible — ask.
## Notes
- One active report per project at a time by design (`.sitrep-panel/report/`
is a fixed path) — the URL stays stable across a whole session. Use
`archive` to start clean.
- Genuinely static HTML plus polling JS — no build step, no external
requests, works fully offline once loaded.
- If the user wants something shareable with people not at their machine,
that's a hosted artifact, not this — this is specifically for
local/private, zero-publish visibility.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Dave
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+111
View File
@@ -0,0 +1,111 @@
# sitrep-panel
**Watch your AI agent work, live, in a browser tab — instead of scrolling a transcript.**
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Claude Code Skill](https://img.shields.io/badge/Claude%20Code-Skill-6c9bff)](https://github.com/dbl8005/sitrep-panel)
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/report-dark.png">
<img src="docs/screenshots/report-light.png" alt="A sitrep-panel report showing a status board, a running narrative, a screenshot, and a newest-first log for a fictional 'add rate limiting' task">
</picture>
</p>
<p align="center"><i>Demo content — no real project, code, or data. Generated for this README.</i></p>
## Why
Long agent sessions are hard to watch. The transcript scrolls faster than you
can read it, and there's no single place that answers "what is it doing
right now, and how did it get here?" without interrupting to ask.
sitrep-panel is that place — a local HTML report the agent writes to itself
as it works, that live-reloads the instant it's updated. Not a chat log:
a running account of the actual state of the work.
It is **not** a hosted artifact or a cloud dashboard. It never leaves your
machine, needs no publish step, no account, and no network access —
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.
- **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
their own gallery.
- **Log** — one entry per real step, newest first: what happened, what was
decided, what was verified.
## How it live-reloads
No WebSocket, no build step, no bundler. The page polls a small `meta.json`
file every 2 seconds and reloads the instant its timestamp changes — the
agent just touches that one file after every content update. Everything
else is plain static HTML served by Python's stdlib `http.server`.
## Install
```bash
claude plugin marketplace add dbl8005/sitrep-panel
claude plugin install sitrep-panel@sitrep-panel
```
Or clone and point Claude Code at it locally:
```bash
git clone https://github.com/dbl8005/sitrep-panel.git
claude plugin marketplace add ./sitrep-panel
claude plugin install sitrep-panel@sitrep-panel
```
## Usage
```
/sitrep-panel
```
Starts (or resumes) a report for the current project and prints its
`http://localhost:<port>/` URL.
```
/sitrep-panel open # just print the URL, don't touch content
/sitrep-panel archive # move the current report aside, start a fresh one
/sitrep-panel stop # stop the local server (content stays on disk)
```
Everything lives at `.sitrep-panel/` in the project you invoke it from —
gitignored automatically, since it's scratch, not history you commit.
Reports are single-project and single-active-report by design: the URL
stays stable for a whole session instead of minting a new page per task.
## Design principles
- **Local-first, always.** No account, no upload, no third-party server in
the loop. The report never exists anywhere but your machine.
- **Never fabricate.** The status board reflects real tracker data or a
manually-kept checklist — never invented tickets or guessed status.
- **Update, don't rewrite.** The protocol is additive (prepend log entries,
replace the current-work block, touch one timestamp file) so a crashed or
interrupted agent leaves a readable partial report, not a blank page.
- **Zero dependencies.** The server is stdlib Python. The page is HTML, CSS,
and about 20 lines of vanilla JS. Nothing to install, nothing to break.
## Contributing
Issues and PRs welcome. `python3 scripts/validate-package.py` checks the
package files; CI runs it plus a real server-start smoke test on every PR.
## Versions
- **1.0.0** — initial release: status board, live "what's happening now"
narrative, screenshot gallery, newest-first log, live-reload, optional
tracker-synced board.
## License
MIT
+143
View File
@@ -0,0 +1,143 @@
---
name: sitrep-panel
description: |
Keep a live local HTML report updated as you work on a task — a status
board, a running "what's happening now" narrative, screenshots, and a
newest-first log — served on localhost so the user can watch progress in
a browser tab instead of reading the transcript. Use when the user invokes
/sitrep-panel, asks for a "progress report", "status page", "live doc",
or wants visibility into a multi-step task as it happens.
license: MIT
metadata:
version: "1.0.0"
---
# sitrep-panel
A live local HTML report you keep updated as you work. It lives at
`.sitrep-panel/report/` inside whatever project you invoke it from, served
on localhost, and it live-reloads in the browser every time you update it —
the user never has to hit refresh.
This is explicitly **not** a Claude Artifact. It never leaves the user's
machine, needs no publish step, and works offline. Use it whenever the user
wants to *watch* a task happen rather than read about it after the fact.
## Invocation
`/sitrep-panel [start | open | stop | archive]` — default (no argument) is
`start`.
- **`start`** (default) — scaffold a new report if none is active, or resume
the active one. Always ends by printing the localhost URL prominently.
- **`open`** — just print the URL of the active report (start the server if
it isn't running); never touches content.
- **`archive`** — move the current active report to
`.sitrep-panel/archive/<timestamp>-<slug>/` and scaffold a fresh one. Use
this when starting genuinely new, unrelated work rather than continuing
the current task.
- **`stop`** — kill the background server. Content on disk is untouched;
`start` or `open` later brings it back.
## First run — scaffold
If `.sitrep-panel/report/` doesn't exist yet in the current project:
1. Create `.sitrep-panel/report/screenshots/`.
2. Copy `assets/template.html` (from this skill's own directory) to
`.sitrep-panel/report/index.html`. Replace `{{TITLE}}` with a short name
for the task, and `{{SUBTITLE}}` with one line of context (what this
report is tracking, and when it started).
3. Write `.sitrep-panel/report/meta.json`:
```json
{"updated_at": "<current ISO-8601 UTC timestamp>"}
```
4. If the project is a git repo and `.sitrep-panel/` is not already covered
by `.gitignore`, append it (`echo '.sitrep-panel/' >> .gitignore`) — this
is local scratch, not something to commit. Mention you did this; don't
ask first, it's trivially reversible. If the user tells you they want
report history committed instead, skip this step and say so.
If `.sitrep-panel/report/` **already exists**, skip scaffolding — resume it.
Use `archive` first if this is unrelated new work and you want a clean slate.
## Serving it
Start the bundled server in the background, from this skill's own directory
(so the relative path to `scripts/serve.py` resolves regardless of the
user's cwd):
```bash
python3 <this-skill-dir>/scripts/serve.py <project>/.sitrep-panel/report
```
Run this with your background-execution mechanism (e.g. the Bash tool's
`run_in_background: true`) — it blocks forever. It prints exactly one line,
`SERVING http://localhost:<port>/`, before blocking; capture that line for
the URL. It auto-picks a free port (tries 8934 first, falls back to any free
port on conflict), so it's safe to have several reports running for
different projects at once.
Record the port for reuse: write `.sitrep-panel/report/.server.json`
(`{"port": <port>, "started_at": "<iso8601>"}`) right after start, gitignored
along with the rest of `.sitrep-panel/`. On a later `start`/`open` in the
same or a resumed session, read this file first — if a `GET` to
`http://localhost:<port>/meta.json` succeeds, the server is already up,
reuse that URL instead of spawning a second one.
**Always tell the user the URL after `start` or `open`, even if you just
reused an existing server.** This is the whole point of the skill — don't
bury it in a paragraph.
## The update protocol — what makes this useful
This is the part that matters. A sitrep-panel that only gets written once at
the start is worthless. Update it at every real step, not just at the end:
1. **Update "What's happening now"** (`#current-work-body`) with prose — what
you're doing right now and *why*, not just a status word. Replace the
whole block's content each time; this section 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, 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.
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`).
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
content write would show a stale reload. This is the one step you must
never skip.
## Tracker-synced board (optional)
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
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
a plain manually-maintained checklist — steps you define yourself, kept in
sync by hand. **Never fabricate ticket data to fill the board**, and don't
silently guess which tracker to use — if more than one is plausible, ask.
## Notes
- One active report per project at a time by design (`.sitrep-panel/report/`
is a fixed path) — this keeps the URL stable across a whole session
instead of minting a new one per task. Use `archive` to start clean.
- The report is genuinely static HTML plus polling JS — no build step, no
external requests, works with the browser fully offline once loaded.
- If the user asks for a report shareable with people who aren't at their
machine, that's a Claude Artifact, not this — tell them so; this skill is
specifically for local/private, zero-publish visibility.
+240
View File
@@ -0,0 +1,240 @@
<!doctype html>
<html lang="en">
<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">
<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;
}
@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;
}
}
* { box-sizing: border-box; }
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; }
.updated-badge {
display: inline-block;
font-size: 0.75rem;
color: var(--text-muted);
background: var(--bg-raised);
border: 1px solid var(--border);
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 {
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--bg-raised);
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.45rem 0.75rem;
font-size: 0.85rem;
}
.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;
}
.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);
border: 1px solid var(--border);
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 .placeholder { color: var(--text-muted); font-style: italic; }
.current-work code, .entry code {
background: var(--code-bg);
border-radius: 4px;
padding: 0.1rem 0.35rem;
font-size: 0.85em;
}
.shots-body { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.shots-body img {
max-width: 260px;
max-height: 180px;
border-radius: 8px;
border: 1px solid var(--border);
object-fit: cover;
cursor: zoom-in;
}
.shots-body:empty::after {
content: "No screenshots yet.";
color: var(--text-muted);
font-style: italic;
font-size: 0.85rem;
}
.entry {
border-left: 2px solid var(--border);
padding: 0 0 0 1rem;
margin-bottom: 1.25rem;
position: relative;
}
.entry::before {
content: "";
position: absolute;
left: -5px;
top: 0.35rem;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent);
}
.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:last-child { margin-bottom: 0; }
.entry-body img {
max-width: 100%;
border-radius: 8px;
border: 1px solid var(--border);
margin: 0.5rem 0;
}
#entries:empty::after {
content: "No entries yet.";
color: var(--text-muted);
font-style: italic;
}
</style>
</head>
<body>
<main>
<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>
</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>
-->
</section>
<section class="current-work">
<h2>What's happening now</h2>
<div id="current-work-body">
<p class="placeholder">Nothing yet — this fills in once work starts.</p>
</div>
</section>
<section class="shots">
<h2>Screenshots</h2>
<div class="shots-body" id="shots-body"></div>
</section>
<section class="log">
<h2>Log <span class="hint">(newest first)</span></h2>
<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>
</div>
-->
</div>
</section>
</main>
<script>
(function () {
var badge = document.getElementById('updated-badge');
var known = null;
function fmtAgo(iso) {
var diff = Math.max(0, (Date.now() - new Date(iso).getTime()) / 1000);
if (diff < 5) return 'just now';
if (diff < 60) return Math.floor(diff) + 's ago';
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
return Math.floor(diff / 3600) + 'h ago';
}
function poll() {
fetch('meta.json?_=' + Date.now(), { cache: 'no-store' })
.then(function (res) { return res.json(); })
.then(function (data) {
if (known === null) known = data.updated_at;
if (data.updated_at !== known) { location.reload(); return; }
badge.textContent = 'updated ' + fmtAgo(data.updated_at);
})
.catch(function () { /* server briefly restarting between writes — ignore */ });
}
poll();
setInterval(poll, 2000);
})();
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Serve a sitrep-panel report directory on a free localhost port.
Usage: python3 serve.py <directory> [preferred_port]
Prints exactly one line to stdout on success: `SERVING http://localhost:<port>/`
then blocks, running the HTTP server launch this with the caller's
background/detached mechanism (e.g. Claude Code's Bash tool with
run_in_background: true). Stdlib only, no dependencies.
"""
from __future__ import annotations
import functools
import http.server
import sys
class QuietHandler(http.server.SimpleHTTPRequestHandler):
"""SimpleHTTPRequestHandler that suppresses per-request access logging,
so stdout stays to the one clean SERVING line callers parse."""
def log_message(self, format: str, *args: object) -> None: # noqa: A002
pass
def main() -> None:
if len(sys.argv) < 2:
print("usage: serve.py <directory> [preferred_port]", file=sys.stderr)
sys.exit(1)
directory = sys.argv[1]
preferred_port = int(sys.argv[2]) if len(sys.argv) > 2 else 8934
handler = functools.partial(QuietHandler, directory=directory)
try:
httpd = http.server.ThreadingHTTPServer(("127.0.0.1", preferred_port), handler)
except OSError:
# Preferred port is taken — let the OS assign a free one rather than
# guessing and racing another process for it.
httpd = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler)
port = httpd.server_address[1]
print(f"SERVING http://localhost:{port}/", flush=True)
httpd.serve_forever()
if __name__ == "__main__":
main()
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Check sitrep-panel's package files without external dependencies."""
from __future__ import annotations
import ast
import json
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SKILL = (ROOT / "SKILL.md").read_text(encoding="utf-8")
README = (ROOT / "README.md").read_text(encoding="utf-8")
AGENTS = (ROOT / "AGENTS.md").read_text(encoding="utf-8")
TEMPLATE = (ROOT / "assets" / "template.html").read_text(encoding="utf-8")
PLUGIN = json.loads((ROOT / ".claude-plugin" / "plugin.json").read_text(encoding="utf-8"))
MARKETPLACE = json.loads((ROOT / ".claude-plugin" / "marketplace.json").read_text(encoding="utf-8"))
SERVE_PY = (ROOT / "scripts" / "serve.py").read_text(encoding="utf-8")
def require_match(match: re.Match[str] | None, message: str) -> re.Match[str]:
if match is None:
raise SystemExit(message)
return match
def require(condition: bool, message: str) -> None:
if not condition:
raise SystemExit(message)
# --- SKILL.md frontmatter ---------------------------------------------------
yaml_metadata = require_match(
re.match(r"\A---\n(.*?)\n---\n", SKILL, re.DOTALL),
"SKILL.md must begin with YAML metadata",
).group(1)
require(
re.search(r"(?m)^name:\s*sitrep-panel\s*$", yaml_metadata) is not None,
"SKILL.md metadata must set name: sitrep-panel",
)
require(
re.search(r"(?m)^description:", yaml_metadata) is not None,
"SKILL.md metadata must set a description",
)
require(
re.search(r"(?m)^license:\s*MIT\s*$", yaml_metadata) is not None,
"SKILL.md metadata must set license: MIT",
)
skill_version = require_match(
re.search(r'(?m)^\s+version:\s*["\']([^"\']+)["\']\s*$', yaml_metadata),
"Add metadata.version to SKILL.md",
).group(1)
# --- version consistency across README / plugin.json / marketplace.json ---
readme_version = require_match(
re.search(r"(?m)^- \*\*([0-9]+\.[0-9]+\.[0-9]+)\*\*", README),
"Add a version entry to README.md's Versions section",
).group(1)
require(
skill_version == readme_version == PLUGIN["version"],
f"Version mismatch: SKILL.md={skill_version} README.md={readme_version} plugin.json={PLUGIN['version']}",
)
require(PLUGIN["name"] == "sitrep-panel", "plugin.json name must be sitrep-panel")
require(PLUGIN["license"] == "MIT", "plugin.json license must be MIT")
require(
any(p["name"] == "sitrep-panel" for p in MARKETPLACE["plugins"]),
"marketplace.json must list a sitrep-panel plugin entry",
)
# --- AGENTS.md exists and isn't a stub --------------------------------------
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"):
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")
# --- serve.py is at least syntactically valid Python ------------------------
try:
ast.parse(SERVE_PY, filename="scripts/serve.py")
except SyntaxError as exc: # pragma: no cover - fails loud on purpose
raise SystemExit(f"scripts/serve.py has a syntax error: {exc}") from exc
require("SERVING http://localhost" in SERVE_PY, "serve.py must print the SERVING line callers parse")
print("sitrep-panel package checks passed.")