mirror of
https://github.com/dbl8005/sitrep-panel.git
synced 2026-08-24 07:29:11 +02:00
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:
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": ["./"]
|
||||
}
|
||||
@@ -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"
|
||||
@@ -0,0 +1,3 @@
|
||||
.DS_Store
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,111 @@
|
||||
# sitrep-panel
|
||||
|
||||
**Watch your AI agent work, live, in a browser tab — instead of scrolling a transcript.**
|
||||
|
||||
[](LICENSE)
|
||||
[](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
|
||||
@@ -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.
|
||||
@@ -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 |
@@ -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()
|
||||
@@ -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.")
|
||||
Reference in New Issue
Block a user