Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"hooks": [
{
"type": "command",
"command": "n=$(ls \"${CLAUDE_PROJECT_DIR:-.}\"/.review/notes/*.json 2>/dev/null | wc -l); if [ \"$n\" -gt 0 ]; then echo \"$n open review notes in .review/notes/ (skill handling-review-notes)\"; fi; true"
"command": "p=\"${CLAUDE_PROJECT_DIR:-.}\"; g=$(git -C \"$p\" rev-parse --path-format=absolute --git-common-dir 2>/dev/null); d=\"$(dirname \"$g\")/.papercuts/papercuts.db\"; if [ -n \"$g\" ] && [ -f \"$d\" ]; then n=$(cd \"$p\" && PAPERCUTS_STORE=local PAPERCUTS_DB=\"$d\" papercuts list 2>/dev/null | sed -n 's/^count: \\([0-9]*\\) of.*/\\1/p'); if [ \"${n:-0}\" -gt 0 ]; then echo \"$n open papercuts of the review site (skill handling-review-papercuts)\"; fi; fi; exit 0"
}
]
}
Expand Down
20 changes: 0 additions & 20 deletions .claude/skills/handling-review-notes/SKILL.md

This file was deleted.

30 changes: 30 additions & 0 deletions .claude/skills/handling-review-papercuts/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
name: handling-review-papercuts
description: Use when Tim mentions papercuts or feedback on the review site, when the session start says there are open papercuts, or when you change scripts/review/ or docker-compose.dev.yml.
---

# Handling review papercuts

Tim writes papercuts on the review site with the papercuts widget. The review site is this clone, served by `scripts/review/devserver.py` in the container `t7d-review` (`docker-compose.dev.yml`, host name in `.env` as `REVIEW_HOST`). A second container, `t7d-review-papercuts`, is the relay. It writes the local store `.papercuts/papercuts.db` in this clone. Each papercut keeps the page, the branch, the commit and a picture.

`.github/papercuts.json` sets `"store": "local"`, so `papercuts add`, `list`, `show`, `comment` and `done` use the local store in every worktree. A worktree uses the `.papercuts/` folder of the main clone. No export is needed. Never run these commands with `PAPERCUTS_STORE=github`: that files a GitHub issue on a public repo.

1. The SessionStart hook (`.claude/settings.json`) prints the number of open papercuts. Tell Tim how many are open, and run `papercuts list`.
2. Run `papercuts show <n>` to read one papercut: the text, the page, the branch, the commit and the picture.
3. Do the work in a worktree for the branch of the papercut, in `~/code/worktrees/t7d/<branch with / as ->`. Follow the rules of this file: a branch, `DESIGN.md`, the look at three widths.
4. A fix is one commit on that branch. One commit is one round on the review site. Tim sees the round in the version picker.
5. Run `papercuts comment <n> "<what changed>"` if Tim needs an explanation.
6. Run `papercuts done <n> --fix "<what changed>"` after the commit.

## Approve

Do not run `python3 scripts/review/approve.py <branch>` unless Tim tells you to, in this conversation or in a papercut. The command merges the branch and publishes it to t7d.dev.

- If Tim tells you to approve, run `python3 scripts/review/approve.py <branch>` in your worktree. It writes the request to the main clone.
- The command prints each step of the host. It stops with exit 0 when the round is published.
- A finished text is not an approval. Tell Tim that the round is ready, and let him press Approve.

- Keep the store and its pictures out of git. `.papercuts/`, `.review/` and `.env` are ignored.
- A change to `devserver.py` needs `docker restart t7d-review`. A change to `review.js` or `review.css` needs only a page load.
- The server adds only `connect-src 'self'` to the CSP of a page, and the origin of the relay when `PAPERCUTS_URL` is set. Keep it that way, so that the review site keeps the rules of the real site.
- Tests: `python3 -m unittest discover -s scripts/review -p 'test_*.py'` and `node --test 'scripts/review/test_*.mjs'`.
26 changes: 26 additions & 0 deletions .github/papercuts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"$schema": "https://raw.githubusercontent.com/Tim-Conrad/papercuts/main/schema/papercuts.schema.json",
"fields": [
{
"id": "expected",
"label": "What did you expect?",
"required": false
}
],
"presets": [
{
"kind": "bug",
"text": "This does not work"
},
{
"kind": "ux",
"text": "This is hard to use on the phone"
},
{
"kind": "copy",
"text": "This text is wrong or unclear"
}
],
"labels": [],
"store": "local"
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ __pycache__/
/thoughts/feed.xml
/blog/feed.xml
.review/
.papercuts/
.env
.preview/

Expand Down
10 changes: 7 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ t7d.dev: the personal site of Tim Conrad. Plain HTML, CSS and a little JavaScrip
- `blog/` and `CVE/` only redirect old addresses (the thoughts were a blog until November 2026). Do not add pages there.

<important if="you show Tim a change for review">
Show a changed page as a Claude artifact, not a local server: `python3 scripts/preview-artifact.py <page folder> <out dir>` builds the bundle; publish `<out dir>/index.html` with the Artifact tool and pass the other printed files in `files`. Republish the same `file_path` after each change, so the link stays. The review site (skill `handling-review-notes`) is where Tim leaves notes on his own.
Show a changed page as a Claude artifact, not a local server: `python3 scripts/preview-artifact.py <page folder> <out dir>` builds the bundle; publish `<out dir>/index.html` with the Artifact tool and pass the other printed files in `files`. Republish the same `file_path` after each change, so the link stays. The review site (skill `handling-review-papercuts`) is where Tim leaves papercuts on his own.
</important>

<important if="Tim mentions notes or feedback on the review site, or you change scripts/review/ or docker-compose.dev.yml">
Use the skill `handling-review-notes`. A hook prints the number of open notes at the start of a session.
<important if="Tim mentions papercuts or feedback on the review site, or you change scripts/review/ or docker-compose.dev.yml">
Use the skill `handling-review-papercuts`. A hook prints the number of open papercuts at the start of a session.
</important>

<important if="you think a text is ready to publish, or you consider running scripts/review/approve.py">
Do not run `python3 scripts/review/approve.py <branch>` unless Tim tells you to, in this conversation or in a papercut. The command merges the branch and publishes it to t7d.dev. If you think that a text is ready, tell Tim. He presses Approve on the review site.
</important>

<important if="you deploy, or the live site does not show a change">
Expand Down
34 changes: 25 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,16 @@ It is plain HTML, CSS and a little JavaScript. There is no build step and no dep
| `scripts/ship.sh` | Merges a PR, waits for the deploy and checks t7d.dev |
| `scripts/preview-artifact.py` | Builds a page as a Claude artifact bundle, for a review before it is published |
| `scripts/reels/` | Scripts that film the two games and cut the reels |
| `scripts/review/`, `docker-compose.dev.yml` | The review site: live reload and a note tab. Never published |
| `scripts/review/`, `docker-compose.dev.yml` | The review site: live reload, rounds of a branch, the papercuts widget and the Approve button. Never published |
| `.github/workflows/deploy.yml` | Runs the deploy on every push to `master` |
| `DESIGN.md` | How the site looks and sounds: colours, type, layout, components and the wording rules |
| `CLAUDE.md` | Instructions for Claude Code in this repository |
| `.claude/skills/updating-product-reels/` | Working guide for Claude Code: how to take the reels and pictures again |
| `.claude/skills/adding-a-thought/` | Working guide for Claude Code: how to turn a link into a new thought, and how to revise one |
| `.claude/skills/updating-game-timeline/` | Working guide for Claude Code: how to film the timeline clips of a game page |
| `.claude/skills/handling-review-notes/` | Working guide for Claude Code: how to handle Tim's notes from the review site |
| `.claude/skills/handling-review-papercuts/` | Working guide for Claude Code: how to handle Tim's papercuts from the review site |
| `.claude/rules/pages.md` | Rules for a new page; Claude Code loads them only when it works on an HTML page, the sitemap or the deploy script |
| `.claude/settings.json` | A hook that prints the number of open review notes at the start of a session |
| `.claude/settings.json` | A hook that prints the number of open papercuts at the start of a session |
| `scripts/check.py` | Checks the CSP rules and the sitemap of every page; CI runs it |

## Preview locally
Expand All @@ -63,19 +63,35 @@ python3 -m http.server 8000

Then open <http://localhost:8000>. All paths are absolute, so serve from the repository root.

For live reload and the note tab, run `python3 scripts/review/devserver.py` instead (port 8000).
For live reload, run `python3 scripts/review/devserver.py` instead (port 8000).

## Review site

The review site serves this clone with live reload and a tab »Note« at the right edge. A note keeps the page, the width, the theme and the part that you pointed at. It is a JSON file in `.review/notes/`, and git ignores that folder. The site files on `gh-pages` do not change: `scripts/deploy.sh` never publishes `scripts/`.
The review site serves this clone and its site worktrees with live reload. The site files on `gh-pages` do not change: `scripts/deploy.sh` never publishes `scripts/`.

Each change is a branch with its own worktree in `~/code/worktrees/t7d/<branch with / as ->`. Each commit on the branch is one round. Open a page with `?branch=<branch>` to see that branch. The page then keeps the branch in a cookie.

The bar at the bottom of each page shows the branch and two versions: master, a round, or "Working" for changes that are not committed. By default it compares the newest round with the round before it. "Show changes" marks the changed text in the page: removed words are struck out, new words are underlined. On a phone the bar is one row. A tap on the comparison opens the selects.

Each page also has the papercuts widget. A papercut keeps the page, the branch, the commit and a picture. A relay container writes it to the local store `.papercuts/papercuts.db` of this clone. `.github/papercuts.json` sets `"store": "local"`, so `papercuts list`, `show` and `done` use that store in every worktree, with no env. Git ignores `.papercuts/`.

"Approve round <n>" in the bar publishes the newest round of the branch. It writes a request to `.review/approve/`. On the host, `scripts/review/approve-host.sh` checks the head and the worktree and runs `scripts/check.py`. Then it pushes the branch, opens the PR, runs `scripts/ship.sh <pr> <sha>`, and removes the worktree and the branch. The bar shows each step from `.review/approve-status/`. An agent can do the same with `python3 scripts/review/approve.py <branch>`, also from its worktree. An agent starts Approve only when Tim tells it to, in that conversation or in a papercut.

To run it in a container behind a login proxy:

1. Make the notes folder: `mkdir -p .review/notes`.
2. Write the host name into `.env`: `REVIEW_HOST=<host>`.
3. Start the stack: `docker-compose -p t7d-review -f docker-compose.dev.yml up -d`.
1. Install the host side of the Approve button once: `scripts/review/approve-install.sh`. It needs `gh` on PATH. It makes `.review/approve/`, `.review/approve-status/` and a systemd user unit that starts `approve-host.sh` for each request. To keep the button working after a reboot, run `loginctl enable-linger developer` once.
2. Make the store folder and the worktrees folder: `mkdir -p .papercuts ~/code/worktrees/t7d`.

Run steps 1 and 2 before step 4. If the folders do not exist, Docker makes them as root.
3. Write the settings into `.env`:
- `REVIEW_HOST`: the host name of the review site.
- `REVIEW_PAPERCUTS_HOST`: the host name of the relay.
- `REVIEW_MAIN`: the host path of this clone.
- `REVIEW_WORKTREES`: the host path of the folder with the site worktrees (`~/code/worktrees/t7d`, as a full path).
- `PAPERCUTS_SRC`: the folder of the relay source with its Dockerfile.
4. Start the stack: `docker-compose -p t7d-review -f docker-compose.dev.yml up -d --build`.

`scripts/review/show.py <id>` opens the page of a note and frames its part in a picture. A handled note goes to `.review/notes/done/` with an `"answer"` field, which the tab shows under »My notes«. Tests: `python3 -m unittest scripts/review/test_devserver.py`.
Tests: `python3 -m unittest discover -s scripts/review -p 'test_*.py'` and `node --test 'scripts/review/test_*.mjs'`.

## Deploy

Expand Down
64 changes: 53 additions & 11 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
# The review site: this clone with live reload and the note tab (scripts/review/devserver.py), in the lab behind its
# The review site: this clone with live reload and the papercuts widget (scripts/review/devserver.py), in the lab behind its
# login. GitHub Pages still serves t7d.dev from gh-pages. This stack never touches it.
#
# mkdir -p .review/notes # first: else Docker makes the folder as root
# echo REVIEW_HOST=<host> > .env # the host name of the review site (.env and .review/ are gitignored)
# docker-compose -p t7d-review -f docker-compose.dev.yml up -d
# # first, else Docker makes the folders as root:
# mkdir -p .review/approve .review/approve-status .papercuts ~/code/worktrees/t7d
# scripts/review/approve-install.sh # once: the host side of the Approve button (a systemd user unit)
# # .env (gitignored): REVIEW_HOST=<host>, REVIEW_MAIN=<host path of this clone>,
# # REVIEW_WORKTREES=<host path of the folder with the site worktrees>
# # REVIEW_PAPERCUTS_HOST=<host of the relay>, PAPERCUTS_SRC=<folder of the relay source with its Dockerfile>
# docker-compose -p t7d-review -f docker-compose.dev.yml up -d --build
# docker-compose -p t7d-review -f docker-compose.dev.yml down
#
# The clone is read-only in the container. Only .review/notes is writable: one JSON file per note.
# A checkout of another branch in this folder shows that branch at once, with its name in the note tab.
# The clone and the worktrees are read-only in the container, at their host paths (git needs the same paths).
# The review server writes only the approve request to .review/approve. It reads the status of the host from .review/approve-status.
# The relay container writes only to .papercuts (the local store of papercuts).
# The page shows the branch of ?branch=, else of the cookie, else of the clone. The image adds only the package git.
services:
t7d-review:
image: python:3.12-slim
build:
context: scripts/review
dockerfile: Dockerfile
image: t7d-review:local
container_name: t7d-review
restart: unless-stopped
user: "1001:1001"
command: [python3, /repo/scripts/review/devserver.py, --root, /repo, --notes, /repo/.review/notes, --host, 0.0.0.0, --port, "8000"]
command: [python3, "${REVIEW_MAIN:?set REVIEW_MAIN in .env}/scripts/review/devserver.py", --root, "${REVIEW_MAIN}", --worktrees, "${REVIEW_WORKTREES}", --host, 0.0.0.0, --port, "8000"]
environment:
PYTHONDONTWRITEBYTECODE: "1"
PAPERCUTS_URL: https://${REVIEW_PAPERCUTS_HOST:?set REVIEW_PAPERCUTS_HOST in .env}
read_only: true
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
Expand All @@ -25,8 +35,10 @@ services:
- /tmp:size=8m,mode=1777
logging: { driver: json-file, options: { max-size: "10m", max-file: "3" } }
volumes:
- .:/repo:ro
- ./.review/notes:/repo/.review/notes:rw
- ${REVIEW_MAIN}:${REVIEW_MAIN}:ro
- ${REVIEW_WORKTREES:?set REVIEW_WORKTREES in .env}:${REVIEW_WORKTREES}:ro
- ${REVIEW_MAIN}/.review/approve:${REVIEW_MAIN}/.review/approve:rw
- ${REVIEW_MAIN}/.review/approve-status:${REVIEW_MAIN}/.review/approve-status:ro
networks: [proxy] # no ports: the lab proxy routes to it
labels:
- traefik.enable=true
Expand All @@ -37,9 +49,39 @@ services:
- traefik.http.services.t7d-review.loadbalancer.server.port=8000
- homepage.group=Apps
- homepage.name=t7d.dev (Review)
- homepage.description=Working copy with live reload and notes
- homepage.description=Working copy with live reload and papercuts
- homepage.href=https://${REVIEW_HOST}

t7d-review-papercuts:
build: ${PAPERCUTS_SRC:?set PAPERCUTS_SRC in .env}
container_name: t7d-review-papercuts
restart: unless-stopped
user: "1001:1001"
environment:
PAPERCUTS_STORE: local
PAPERCUTS_DB: /data/papercuts.db
PAPERCUTS_HOST: 0.0.0.0
PAPERCUTS_ORIGINS: https://${REVIEW_HOST}
PAPERCUTS_REPOS: Tim-Conrad/Tim-Conrad.github.io
PAPERCUTS_PUBLIC_URL: https://${REVIEW_PAPERCUTS_HOST}
read_only: true
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
mem_limit: 128m
tmpfs:
- /tmp
logging: { driver: json-file, options: { max-size: "10m", max-file: "3" } }
volumes:
- ${REVIEW_MAIN}/.papercuts:/data:rw
networks: [proxy] # no ports: the lab proxy routes to it
labels:
- traefik.enable=true
- traefik.http.routers.t7d-review-papercuts.rule=Host(`${REVIEW_PAPERCUTS_HOST}`)
- traefik.http.routers.t7d-review-papercuts.entrypoints=websecure
- traefik.http.routers.t7d-review-papercuts.tls=true
- traefik.http.routers.t7d-review-papercuts.middlewares=authelia@file,security-headers@file
- traefik.http.services.t7d-review-papercuts.loadbalancer.server.port=8000

networks:
proxy:
external: true
4 changes: 4 additions & 0 deletions scripts/review/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FROM python:3.12-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends git \
&& rm -rf /var/lib/apt/lists/*
Loading
Loading