From 971760b2ac2651fa18e697a2f3b3c427d5a48dbc Mon Sep 17 00:00:00 2001 From: Tim Conrad <4759569+Tim-Conrad@users.noreply.github.com> Date: Tue, 10 Nov 2026 23:34:43 +0100 Subject: [PATCH 1/4] Move the scripts from tools/ to scripts/, so that /tools/ can be a page Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KFxuEteUiyeGMzLJ2VGnGc --- .claude/rules/pages.md | 6 +-- .claude/skills/adding-a-thought/SKILL.md | 2 +- .claude/skills/handling-review-notes/SKILL.md | 8 ++-- .../skills/updating-game-timeline/SKILL.md | 14 +++--- .../skills/updating-product-reels/SKILL.md | 20 ++++----- .github/workflows/check.yml | 2 +- .github/workflows/deploy.yml | 4 +- .gitignore | 2 +- CLAUDE.md | 12 +++--- README.md | 40 +++++++++--------- docker-compose.dev.yml | 4 +- {tools => scripts}/check.py | 4 +- {tools => scripts}/deploy.sh | 6 +-- {tools => scripts}/gen-feeds.py | 6 +-- {tools => scripts}/preview-artifact.py | 2 +- {tools => scripts}/reels/build.py | 2 +- {tools => scripts}/reels/fonts/OFL.txt | 0 .../reels/fonts/fredoka-500.woff2 | Bin .../reels/fonts/fredoka-600.woff2 | Bin .../reels/fonts/nunito-700.woff2 | Bin {tools => scripts}/reels/kleinbahn-shots.mjs | 2 +- {tools => scripts}/reels/overlays.mjs | 2 +- {tools => scripts}/reels/pw.mjs | 0 .../reels/timeline-kleinbahn.mjs | 2 +- .../reels/timeline-wortzauber.mjs | 2 +- {tools => scripts}/reels/timeline-wz.mjs | 6 +-- {tools => scripts}/reels/timeline.mjs | 6 +-- {tools => scripts}/reels/wortzauber-takes.mjs | 2 +- {tools => scripts}/review/devserver.py | 6 +-- {tools => scripts}/review/review.css | 2 +- {tools => scripts}/review/review.js | 2 +- {tools => scripts}/review/show.py | 2 +- {tools => scripts}/review/test_devserver.py | 2 +- {tools => scripts}/ship.sh | 6 +-- 34 files changed, 88 insertions(+), 88 deletions(-) rename {tools => scripts}/check.py (94%) rename {tools => scripts}/deploy.sh (88%) rename {tools => scripts}/gen-feeds.py (95%) rename {tools => scripts}/preview-artifact.py (98%) rename {tools => scripts}/reels/build.py (98%) rename {tools => scripts}/reels/fonts/OFL.txt (100%) rename {tools => scripts}/reels/fonts/fredoka-500.woff2 (100%) rename {tools => scripts}/reels/fonts/fredoka-600.woff2 (100%) rename {tools => scripts}/reels/fonts/nunito-700.woff2 (100%) rename {tools => scripts}/reels/kleinbahn-shots.mjs (98%) rename {tools => scripts}/reels/overlays.mjs (98%) rename {tools => scripts}/reels/pw.mjs (100%) rename {tools => scripts}/reels/timeline-kleinbahn.mjs (98%) rename {tools => scripts}/reels/timeline-wortzauber.mjs (99%) rename {tools => scripts}/reels/timeline-wz.mjs (94%) rename {tools => scripts}/reels/timeline.mjs (95%) rename {tools => scripts}/reels/wortzauber-takes.mjs (98%) rename {tools => scripts}/review/devserver.py (97%) rename {tools => scripts}/review/review.css (96%) rename {tools => scripts}/review/review.js (98%) rename {tools => scripts}/review/show.py (97%) rename {tools => scripts}/review/test_devserver.py (98%) rename {tools => scripts}/ship.sh (93%) diff --git a/.claude/rules/pages.md b/.claude/rules/pages.md index 7448711..e1706a2 100644 --- a/.claude/rules/pages.md +++ b/.claude/rules/pages.md @@ -2,12 +2,12 @@ paths: - "**/*.html" - "sitemap.xml" - - "tools/deploy.sh" + - "scripts/deploy.sh" --- # A new page, or a new top-level file or folder of the site - Copy the `` of an existing page, with its Content-Security-Policy line. -- Add the URL to `sitemap.xml` (`tools/check.py` fails without it). -- Add a new top-level file or folder to the `SITE` list in `tools/deploy.sh`, or it is not published. +- Add the URL to `sitemap.xml` (`scripts/check.py` fails without it). +- Add a new top-level file or folder to the `SITE` list in `scripts/deploy.sh`, or it is not published. - Update "Contents" in the title block of `index.html` and the sheet numbers of its sections. diff --git a/.claude/skills/adding-a-thought/SKILL.md b/.claude/skills/adding-a-thought/SKILL.md index 4aafd42..d03af4d 100644 --- a/.claude/skills/adding-a-thought/SKILL.md +++ b/.claude/skills/adding-a-thought/SKILL.md @@ -60,7 +60,7 @@ Then: Follow CLAUDE.md: serve the site (`python3 -m http.server 8000`), look at 1440, 390 and 320 px in both themes, no sideways scroll, no console errors. Click every switch and pick, and look at the narrow drawings. Commit on the session's branch and push. -Tim reviews a thought as a Claude artifact before it is published: run `python3 tools/preview-artifact.py thoughts/ /preview-` and publish its `index.html` with the Artifact tool, with every other printed file in `files`. After a change, build again and publish the same `file_path`, so the link stays the same. Comments on the artifact reach the session: answer them and change the page in the repository, never only in the bundle. `tools/ship.sh ` merges and checks the live site when Tim asks for it. +Tim reviews a thought as a Claude artifact before it is published: run `python3 scripts/preview-artifact.py thoughts/ /preview-` and publish its `index.html` with the Artifact tool, with every other printed file in `files`. After a change, build again and publish the same `file_path`, so the link stays the same. Comments on the artifact reach the session: answer them and change the page in the repository, never only in the bundle. `scripts/ship.sh ` merges and checks the live site when Tim asks for it. ## Revising a thought diff --git a/.claude/skills/handling-review-notes/SKILL.md b/.claude/skills/handling-review-notes/SKILL.md index d6f2012..41e339e 100644 --- a/.claude/skills/handling-review-notes/SKILL.md +++ b/.claude/skills/handling-review-notes/SKILL.md @@ -1,15 +1,15 @@ --- name: handling-review-notes -description: Use when Tim mentions notes or feedback on the review site (the tab »Note«), when the session start says there are open review notes, or when you change tools/review/ or docker-compose.dev.yml. +description: Use when Tim mentions notes or feedback on the review site (the tab »Note«), when the session start says there are open review notes, or when you change scripts/review/ or docker-compose.dev.yml. --- # Handling review notes -Tim writes notes on the review site: the tab »Note« at the right edge. The review site is this clone, served by `tools/review/devserver.py` in the container `t7d-review` (`docker-compose.dev.yml`, host name in `.env` as `REVIEW_HOST`). It shows the checked-out branch at once and loads the page again when a file changes. A note is a JSON file in `.review/notes/`. +Tim writes notes on the review site: the tab »Note« at the right edge. 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`). It shows the checked-out branch at once and loads the page again when a file changes. A note is a JSON file in `.review/notes/`. 1. The SessionStart hook (`.claude/settings.json`) prints the number of open notes in `.review/notes/`. Tell Tim how many are open, and list them with `ls .review/notes/*.json`. 2. Read the note: `note`, `path`, `viewport`, `theme`, `scroll`, `target` (`selector`, `text`, `rect`), `selection`, `branch`, `commit`. -3. Run `uv run --no-project --with playwright python3 tools/review/show.py ` (more ids in one call). Look at `.review/notes/.png`. The red frame is the part that Tim pointed at. +3. Run `uv run --no-project --with playwright python3 scripts/review/show.py ` (more ids in one call). Look at `.review/notes/.png`. The red frame is the part that Tim pointed at. 4. Do the work with the rules of this file: a branch, `DESIGN.md`, the look at three widths. 5. After the fix, run `show.py --suffix after` and compare the two pictures. 6. Add an `"answer"` field to the note: what changed, and the commit. Move the note to `.review/notes/done/`. Tim sees the answer under »My notes« in the tab. @@ -17,4 +17,4 @@ Tim writes notes on the review site: the tab »Note« at the right edge. The rev - Keep notes and their pictures out of git. `.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. Keep it that way, so that the review site keeps the rules of the real site. -- Tests: `python3 -m unittest tools/review/test_devserver.py`. +- Tests: `python3 -m unittest scripts/review/test_devserver.py`. diff --git a/.claude/skills/updating-game-timeline/SKILL.md b/.claude/skills/updating-game-timeline/SKILL.md index 170fe07..9cc7606 100644 --- a/.claude/skills/updating-game-timeline/SKILL.md +++ b/.claude/skills/updating-game-timeline/SKILL.md @@ -10,8 +10,8 @@ The product page of a game has a section "How it changed": one entry per merged | Game | Stages | Clips | Page | | --- | --- | --- | --- | -| Kleinbahn | `tools/reels/timeline-kleinbahn.mjs`, filmed by `timeline.mjs` | `assets/projects/timeline/kleinbahn-.mp4` and `.jpg` | `projects/kleinbahn/index.html`, section `#changes` | -| Wortzauber | `tools/reels/timeline-wortzauber.mjs`, filmed by `timeline-wz.mjs` | `assets/projects/timeline/wortzauber-.mp4` and `.jpg` | `projects/wortzauber/index.html`, section `#changes` | +| Kleinbahn | `scripts/reels/timeline-kleinbahn.mjs`, filmed by `timeline.mjs` | `assets/projects/timeline/kleinbahn-.mp4` and `.jpg` | `projects/kleinbahn/index.html`, section `#changes` | +| Wortzauber | `scripts/reels/timeline-wortzauber.mjs`, filmed by `timeline-wz.mjs` | `assets/projects/timeline/wortzauber-.mp4` and `.jpg` | `projects/wortzauber/index.html`, section `#changes` | ## Rules - **One clip per PR, the coolest change.** Pick the change a player sees and enjoys most: something that moves on the island beats a panel, a panel beats a fix. List the rest of the PR in one short line each. @@ -27,14 +27,14 @@ Do not read the code of every PR. Work from the git log. 3. Read each new PR's commit messages only: `git -C ../tiny-train log --format='%B' ^1..^2`. They name every change. Decide: entry or "Not shown", and which change to film. 4. Find the names a shot needs with one `git grep` at that ref (`git -C ../tiny-train grep -n 'function aStartFlight' -- src/game`), not by reading files. 5. Add one stage per new entry to the stages file (fields: see the comment at its top). Copy the closest existing stage: an event (`setup` + `until` + `cam: null` to follow), a camera move (`cam`), a UI moment (`ui: true` and an `each` that presses a button), shared play (`net: 'host'` or `'guest'`). -6. Test only the new stages, three frames each: `node tools/reels/timeline.mjs ../tiny-train --test --only pr14`. Join the frames into one sheet and read it (`ffmpeg -i /tl/kb-test/pr14/%04d.jpg -vf tile=3x1 sheet.jpg`). -7. Film them: `node tools/reels/timeline.mjs ../tiny-train --only pr14` (about 1 to 3 s a frame; a stage of 96 frames takes 2 to 5 min). -8. Cut: `python3 tools/reels/build.py timeline` writes the clips and posters of every filmed stage into `assets/projects/timeline/` (640x360, about 100 to 400 KB each). +6. Test only the new stages, three frames each: `node scripts/reels/timeline.mjs ../tiny-train --test --only pr14`. Join the frames into one sheet and read it (`ffmpeg -i /tl/kb-test/pr14/%04d.jpg -vf tile=3x1 sheet.jpg`). +7. Film them: `node scripts/reels/timeline.mjs ../tiny-train --only pr14` (about 1 to 3 s a frame; a stage of 96 frames takes 2 to 5 min). +8. Cut: `python3 scripts/reels/build.py timeline` writes the clips and posters of every filmed stage into `assets/projects/timeline/` (640x360, about 100 to 400 KB each). 9. Add the entry to `projects/kleinbahn/index.html`: copy the first `
  • ` of `.tl` and put the new one above it (newest first). Date, PR number, title, the sentence on the clip, the list of the other changes. 10. Serve the site and check `/projects/kleinbahn/#changes` at 1440, 390 and 320 px, light and dark. Commit the stages file, the clips and the page. ## The timeline script -- `tools/reels/timeline.mjs` checks out each stage's `ref` as a git worktree in `/tl/wt/`, links `node_modules` from the game repo, opens that version through its own `tools/page.mjs` (virtual clock, seeded random), films the frames and removes the worktree. `` is `$REELS_WORK` or `.reels/`. +- `scripts/reels/timeline.mjs` checks out each stage's `ref` as a git worktree in `/tl/wt/`, links `node_modules` from the game repo, opens that version through its own `tools/page.mjs` (virtual clock, seeded random), films the frames and removes the worktree. `` is `$REELS_WORK` or `.reels/`. - One frame is one `__KB.step(1, 1/24)`. A canvas stage reads the WebGL canvas; a `ui` stage takes a page screenshot, so the controls and toasts show. - `net: 'host'`: the game hosts and `window.__peer` is a guest (`__peer.world` its island, `__peer.sync` its engine). Change `__peer.world.layers` and the host gets it next frame; `__peer.sync.presence({...})` moves its pin. - `net: 'guest'`: the game joins `__peer`, a host whose island is the game's own island without trees. `__peer.sync.event(k, id, e, d)` starts a host event (eruption, launch, fire); `__peer.sync.leave()` closes the room. @@ -42,7 +42,7 @@ Do not read the code of every PR. Work from the git log. - Node 22 runs the script. Run nothing heavy next to it, and never next to a Wortzauber recording. ## Wortzauber -- The game runs in real time, so each stage is a screen recording: `node tools/reels/timeline-wz.mjs ../wortzauber [--dry] [--only pr65]`. `--dry` saves one still per stage into `/tl/wz-dry/`; read them as one sheet before you record. All stages take about 5 minutes. +- The game runs in real time, so each stage is a screen recording: `node scripts/reels/timeline-wz.mjs ../wortzauber [--dry] [--only pr65]`. `--dry` saves one still per stage into `/tl/wz-dry/`; read them as one sheet before you record. All stages take about 5 minutes. - Each worktree is served with `python3 -m http.server` on its own port. That is fine here: every stage opens a fresh browser, so nothing is cached. (For normal work on the game, use its `devserver.py`.) - The cheapest stage uses a layout-check scene of the game at that ref (`scene: 'castle'`): a feature PR often added its own scene, see `.claude/skills/checking-device-layouts/scenes/` in the game and `git log --diff-filter=A` on it. Else a short `setup` with `WZ.jump(...)` (from PR #31 on), `WZ.useWord(...)`, `Wall`, `Castle`, `Together.testLink()`; an `act` films the action itself. - The clips are silent. A PR that only adds sound goes into the "Not shown" line, and so does a change that opens only deep in a level. diff --git a/.claude/skills/updating-product-reels/SKILL.md b/.claude/skills/updating-product-reels/SKILL.md index b1bf8ba..761c06c 100644 --- a/.claude/skills/updating-product-reels/SKILL.md +++ b/.claude/skills/updating-product-reels/SKILL.md @@ -6,7 +6,7 @@ description: Use when Kleinbahn (tiny-train) or Wortzauber changed and the reel, # Updating the product reels and pictures ## Overview -Each game has a product page under `projects//` with a reel, a poster, four stills and a phone frame that loads the live demo. Everything visual comes from the real game, filmed headless by the scripts in `tools/reels/`. Nothing is drawn or generated. +Each game has a product page under `projects//` with a reel, a poster, four stills and a phone frame that loads the live demo. Everything visual comes from the real game, filmed headless by the scripts in `scripts/reels/`. Nothing is drawn or generated. | Game | Repo | Product page | Live demo in the phone frame | | --- | --- | --- | --- | @@ -18,7 +18,7 @@ Both game repos are private. Clone them next to this repo first (in a cloud sess ## What needs a new capture - The live demo in the phone frame updates by itself when the game is published to beacon.host. Only its poster picture is from here. - A visual change of the game (art, UI, a scene in a shot): film that game again, then build and publish. -- Only a caption or a title: change the text in `tools/reels/overlays.mjs`, then run overlays, build and publish. No filming. +- Only a caption or a title: change the text in `scripts/reels/overlays.mjs`, then run overlays, build and publish. No filming. - Only the page text: edit `projects//index.html`. The text must stay true to the game: read its `README.md` before you claim a feature. - Wording, on the pages and in the captions: plain and down to earth. A caption names what the shot shows ("Trains", "Shuttle launch"). No slogans and no superlatives. @@ -27,13 +27,13 @@ All of them write to a work folder: `$REELS_WORK`, or `.reels/` in this repo (gi | Step | Command | Time | | --- | --- | --- | -| Kleinbahn preview | `node tools/reels/kleinbahn-shots.mjs ../tiny-train --test` | 1 min | -| Kleinbahn frames | `node tools/reels/kleinbahn-shots.mjs ../tiny-train [--only b-train,c-town]` | 25 to 40 min for all nine shots | -| Wortzauber dry run | `node tools/reels/wortzauber-takes.mjs ../wortzauber` | 3 min | -| Wortzauber recording | `node tools/reels/wortzauber-takes.mjs ../wortzauber --rec [--only meadow]` | 3 min | -| Titles and captions | `node tools/reels/overlays.mjs ../wortzauber` | seconds | -| Cut a reel | `python3 tools/reels/build.py kb` and `python3 tools/reels/build.py wz ../wortzauber` | 1 to 2 min each | -| Put it on the site | `python3 tools/reels/build.py publish` | 1 min | +| Kleinbahn preview | `node scripts/reels/kleinbahn-shots.mjs ../tiny-train --test` | 1 min | +| Kleinbahn frames | `node scripts/reels/kleinbahn-shots.mjs ../tiny-train [--only b-train,c-town]` | 25 to 40 min for all nine shots | +| Wortzauber dry run | `node scripts/reels/wortzauber-takes.mjs ../wortzauber` | 3 min | +| Wortzauber recording | `node scripts/reels/wortzauber-takes.mjs ../wortzauber --rec [--only meadow]` | 3 min | +| Titles and captions | `node scripts/reels/overlays.mjs ../wortzauber` | seconds | +| Cut a reel | `python3 scripts/reels/build.py kb` and `python3 scripts/reels/build.py wz ../wortzauber` | 1 to 2 min each | +| Put it on the site | `python3 scripts/reels/build.py publish` | 1 min | `publish` writes `assets/projects/-reel.mp4` (web size), `-poster.jpg`, `-1.jpg` to `-4.jpg`, `-loop.webp` (the gameplay loop of the game box on the front page) and `wortzauber-phone.jpg`. `kleinbahn-phone.jpg` is a separate step (below). @@ -60,7 +60,7 @@ Before a run: start the dev server in `../wortzauber` with `python3 devserver.py - Rules of the game repo that hold here too: do not generate voice, music or images, and do not read the ElevenLabs key. The reels are silent: the site plays every video as a muted loop. ## Build and check -1. `overlays.mjs` renders every title, caption and end card as a transparent 1280x720 PNG, in the fonts of each game (Fredoka and Nunito from `tools/reels/fonts/`, Andika from the Wortzauber repo). +1. `overlays.mjs` renders every title, caption and end card as a transparent 1280x720 PNG, in the fonts of each game (Fredoka and Nunito from `scripts/reels/fonts/`, Andika from the Wortzauber repo). 2. `build.py kb` and `build.py wz` cut the reels. The timeline of a reel is the `parts` list in `kb()` and `wz()`: one `seg` or `clip` per cut, with its overlays and their fade times. 3. Look at the result before you publish. Make a contact sheet and read it: `ffmpeg -i /kleinbahn-reel.mp4 -vf "fps=16/39,scale=320:180,tile=4x4" -frames:v 1 sheet.jpg` (16 pictures over a 39 s reel). diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 71ee819..56df3e2 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -12,4 +12,4 @@ jobs: steps: - uses: actions/checkout@v4 - name: CSP rules and sitemap - run: python3 tools/check.py + run: python3 scripts/check.py diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 092aeb0..bdb7500 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -18,9 +18,9 @@ jobs: steps: - uses: actions/checkout@v4 - name: Check the pages - run: python3 tools/check.py + run: python3 scripts/check.py - name: Publish the site files to gh-pages env: GIT_AUTHOR_NAME: github-actions[bot] GIT_AUTHOR_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com - run: tools/deploy.sh + run: scripts/deploy.sh diff --git a/.gitignore b/.gitignore index 5a4d505..d6bfe8a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .reels/ __pycache__/ -# made by tools/gen-feeds.py at deploy time +# made by scripts/gen-feeds.py at deploy time /llms.txt /thoughts/feed.xml /blog/feed.xml diff --git a/CLAUDE.md b/CLAUDE.md index 3ced004..19044b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ t7d.dev: the personal site of Tim Conrad. Plain HTML, CSS and a little JavaScrip ## Check your work -- `python3 tools/check.py`: the Content-Security-Policy rules and the sitemap of every page. CI runs it on pull requests and before each deploy. +- `python3 scripts/check.py`: the Content-Security-Policy rules and the sitemap of every page. CI runs it on pull requests and before each deploy. - After a change to a page, a style or any wording: serve the site (`python3 -m http.server 8000` in the repository root; all paths are absolute) and look at it at 1440, 390 and 320 px wide, in the light and the dark theme. The page must not scroll sideways, and the browser console must show no errors. - Show what you ran and what you saw, not only "it works". @@ -13,21 +13,21 @@ t7d.dev: the personal site of Tim Conrad. Plain HTML, CSS and a little JavaScrip - Read `DESIGN.md` before you change a page, a style or any wording. The two rules that matter most: the site is black and white with colour only in pictures and videos, and the wording is plain with no slogans or overstatement. - Claim only what is true. Do not invent facts about Tim, the research or the games. - Do not rewrite the text of the write-up under `research/`. It is Tim's own. -- A new or revised thought: use the skill `adding-a-thought`. `llms.txt` and the feeds are made at deploy time (`tools/gen-feeds.py`); do not commit them. +- A new or revised thought: use the skill `adding-a-thought`. `llms.txt` and the feeds are made at deploy time (`scripts/gen-feeds.py`); do not commit them. - Keep the Content-Security-Policy of each page strict: no inline scripts or styles, no `style=""` attributes, and no request to another site on page load. `assets/theme.js` and `assets/site.js` are the only scripts. -- `master` is the source. Never edit `gh-pages` by hand: the workflow publishes it after every push to `master`, and the sealed admin app (`Tim-Conrad/sealed`) publishes encrypted sites to `sealed/` on it, a folder that exists only there. `tools/deploy.sh` must keep `sealed/`. +- `master` is the source. Never edit `gh-pages` by hand: the workflow publishes it after every push to `master`, and the sealed admin app (`Tim-Conrad/sealed`) publishes encrypted sites to `sealed/` on it, a folder that exists only there. `scripts/deploy.sh` must keep `sealed/`. - Classes and scripts that the sealed login page uses (`.sealed`, `.label`, `.btn`, `.nav`, `#theme-toggle`, `/assets/site.js`) are used by another repository too. Keep them, or change `client/themes/t7d/login.html` in sealed with them. - `blog/` and `CVE/` only redirect old addresses (the thoughts were a blog until November 2026). Do not add pages there. -Show a changed page as a Claude artifact, not a local server: `python3 tools/preview-artifact.py ` builds the bundle; publish `/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 ` builds the bundle; publish `/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. - + Use the skill `handling-review-notes`. A hook prints the number of open notes at the start of a session. See "Deploy" in `README.md`. GitHub Pages serves the `gh-pages` branch at https://t7d.dev. -When Tim asks to ship a PR, run `tools/ship.sh ` after the checks of the change. Merging publishes to t7d.dev, so do not run it unasked. If it stops, read its last line, fix the cause, and run it again: a merged PR skips the merge. +When Tim asks to ship a PR, run `scripts/ship.sh ` after the checks of the change. Merging publishes to t7d.dev, so do not run it unasked. If it stops, read its last line, fix the cause, and run it again: a merged PR skips the merge. diff --git a/README.md b/README.md index b8ddef4..36d24cf 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,11 @@ It is plain HTML, CSS and a little JavaScript. There is no build step and no dep | `assets/projects/` | Reels, posters, stills, phone pictures and gameplay loops of the two games | | `assets/og.png` | Social preview image | | `CNAME`, `.nojekyll`, `robots.txt`, `sitemap.xml` | Hosting and crawler files | -| `tools/deploy.sh` | Publishes the site files to `gh-pages` | -| `tools/ship.sh` | Merges a PR, waits for the deploy and checks t7d.dev | -| `tools/preview-artifact.py` | Builds a page as a Claude artifact bundle, for a review before it is published | -| `tools/reels/` | Scripts that film the two games and cut the reels | -| `tools/review/`, `docker-compose.dev.yml` | The review site: live reload and a note tab. Never published | +| `scripts/deploy.sh` | Publishes the site files to `gh-pages` | +| `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 | | `.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 | @@ -51,7 +51,7 @@ It is plain HTML, CSS and a little JavaScript. There is no build step and no dep | `.claude/skills/handling-review-notes/` | Working guide for Claude Code: how to handle Tim's notes 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 | -| `tools/check.py` | Checks the CSP rules and the sitemap of every page; CI runs it | +| `scripts/check.py` | Checks the CSP rules and the sitemap of every page; CI runs it | ## Preview locally @@ -61,11 +61,11 @@ python3 -m http.server 8000 Then open . All paths are absolute, so serve from the repository root. -For live reload and the note tab, run `python3 tools/review/devserver.py` instead (port 8000). +For live reload and the note tab, 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: `tools/deploy.sh` never publishes `tools/`. +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/`. To run it in a container behind a login proxy: @@ -73,21 +73,21 @@ To run it in a container behind a login proxy: 2. Write the host name into `.env`: `REVIEW_HOST=`. 3. Start the stack: `docker-compose -p t7d-review -f docker-compose.dev.yml up -d`. -`tools/review/show.py ` 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 tools/review/test_devserver.py`. +`scripts/review/show.py ` 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`. ## Deploy GitHub Pages serves the `gh-pages` branch at . `master` is the source, and nobody edits `gh-pages` by hand. 1. Merge a change into `master`. -2. The workflow `.github/workflows/deploy.yml` runs `tools/deploy.sh`. -3. The script copies the site files of that commit to `gh-pages`, writes `llms.txt` and the feeds from the pages with `tools/gen-feeds.py`, and pushes. `tools/`, `.claude/`, `.github/` and this README stay on `master`. +2. The workflow `.github/workflows/deploy.yml` runs `scripts/deploy.sh`. +3. The script copies the site files of that commit to `gh-pages`, writes `llms.txt` and the feeds from the pages with `scripts/gen-feeds.py`, and pushes. `scripts/`, `.claude/`, `.github/` and this README stay on `master`. -To ship a pull request in one step, run `tools/ship.sh []`. It merges the PR, puts this clone on `master`, waits for the deploy and the Pages build, and checks that t7d.dev serves each site file of the PR. It runs no checks: look at the pages first. +To ship a pull request in one step, run `scripts/ship.sh []`. It merges the PR, puts this clone on `master`, waits for the deploy and the Pages build, and checks that t7d.dev serves each site file of the PR. It runs no checks: look at the pages first. -The folder `sealed/` on `gh-pages` does not come from `master`. The admin app of sealed in the lab builds each encrypted site and pushes it to `gh-pages` as `sealed//`: a login page, a keyring and ciphertext, never the plain files. `tools/deploy.sh` keeps that folder as it is, and sealed changes nothing outside it. Users, passwords and access are managed in the sealed admin app, not here. The setup is in "Publish below t7d.dev" in the README of sealed. +The folder `sealed/` on `gh-pages` does not come from `master`. The admin app of sealed in the lab builds each encrypted site and pushes it to `gh-pages` as `sealed//`: a login page, a keyring and ciphertext, never the plain files. `scripts/deploy.sh` keeps that folder as it is, and sealed changes nothing outside it. Users, passwords and access are managed in the sealed admin app, not here. The setup is in "Publish below t7d.dev" in the README of sealed. -To deploy by hand, run `tools/deploy.sh` on `master`. To look at `llms.txt` and the feed locally, run `python3 tools/gen-feeds.py` (git ignores the two files). A new top-level file or folder that belongs to the site must be added to the `SITE` list in that script, or it is not published. +To deploy by hand, run `scripts/deploy.sh` on `master`. To look at `llms.txt` and the feed locally, run `python3 scripts/gen-feeds.py` (git ignores the two files). A new top-level file or folder that belongs to the site must be added to the `SITE` list in that script, or it is not published. ## Common changes @@ -102,12 +102,12 @@ To deploy by hand, run `tools/deploy.sh` on `master`. To look at `llms.txt` and The live demo updates by itself when the game is published to beacon.host. The reel, the poster, the stills and the phone picture are files in `assets/projects/` and must be taken again: ```sh -node tools/reels/kleinbahn-shots.mjs ../tiny-train # frames of the Kleinbahn shots -node tools/reels/wortzauber-takes.mjs ../wortzauber --rec # recordings of the Wortzauber takes -node tools/reels/overlays.mjs ../wortzauber # titles and captions -python3 tools/reels/build.py kb -python3 tools/reels/build.py wz ../wortzauber -python3 tools/reels/build.py publish # writes assets/projects/ +node scripts/reels/kleinbahn-shots.mjs ../tiny-train # frames of the Kleinbahn shots +node scripts/reels/wortzauber-takes.mjs ../wortzauber --rec # recordings of the Wortzauber takes +node scripts/reels/overlays.mjs ../wortzauber # titles and captions +python3 scripts/reels/build.py kb +python3 scripts/reels/build.py wz ../wortzauber +python3 scripts/reels/build.py publish # writes assets/projects/ ``` This needs Node with Playwright, ffmpeg, and both game repositories cloned next to this one. The skill in `.claude/skills/updating-product-reels/SKILL.md` has the details, the checks and the pitfalls. diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 4753530..bb7a199 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,4 +1,4 @@ -# The review site: this clone with live reload and the note tab (tools/review/devserver.py), in the lab behind its +# The review site: this clone with live reload and the note tab (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 @@ -14,7 +14,7 @@ services: container_name: t7d-review restart: unless-stopped user: "1001:1001" - command: [python3, /repo/tools/review/devserver.py, --root, /repo, --notes, /repo/.review/notes, --host, 0.0.0.0, --port, "8000"] + command: [python3, /repo/scripts/review/devserver.py, --root, /repo, --notes, /repo/.review/notes, --host, 0.0.0.0, --port, "8000"] environment: PYTHONDONTWRITEBYTECODE: "1" read_only: true diff --git a/tools/check.py b/scripts/check.py similarity index 94% rename from tools/check.py rename to scripts/check.py index cc71e2d..93a52b6 100644 --- a/tools/check.py +++ b/scripts/check.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Checks the rules of the site pages that must always hold. CI runs it before each deploy and on each pull request. - python3 tools/check.py + python3 scripts/check.py For every HTML page in git (redirect pages excepted, they only carry a meta refresh): - a Content-Security-Policy is there; @@ -19,7 +19,7 @@ def pages(): out = subprocess.run(['git', '-C', ROOT, 'ls-files', '*.html'], capture_output=True, text=True, check=True).stdout - return [p for p in out.split() if not p.startswith(('tools/', '.claude/'))] + return [p for p in out.split() if not p.startswith(('scripts/', '.claude/'))] def main(): diff --git a/tools/deploy.sh b/scripts/deploy.sh similarity index 88% rename from tools/deploy.sh rename to scripts/deploy.sh index ee328d3..512cdec 100755 --- a/tools/deploy.sh +++ b/scripts/deploy.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Publishes the site files of the current commit to the gh-pages branch, which GitHub Pages serves at t7d.dev. -# Only the site goes there: tools/, .claude/, .github/ and the README stay on master. -# The workflow .github/workflows/deploy.yml runs this on every push to master. To run it by hand: tools/deploy.sh +# Only the site goes there: scripts/, .claude/, .github/ and the README stay on master. +# The workflow .github/workflows/deploy.yml runs this on every push to master. To run it by hand: scripts/deploy.sh # # The folder sealed/ on gh-pages is not from master: the admin app of sealed (Tim-Conrad/sealed) pushes the # encrypted sites there, one folder per site (sealed//). This script keeps that folder as it is. @@ -22,7 +22,7 @@ if git cat-file -e HEAD:sealed 2>/dev/null; then fi git -C "$root" archive "$src" "${SITE[@]}" | tar -x # llms.txt and the RSS feed of the thoughts are made from the published pages, never by hand -python3 "$root/tools/gen-feeds.py" "$tree" +python3 "$root/scripts/gen-feeds.py" "$tree" git add -A if git diff --cached --quiet; then diff --git a/tools/gen-feeds.py b/scripts/gen-feeds.py similarity index 95% rename from tools/gen-feeds.py rename to scripts/gen-feeds.py index 052a4bd..d08cbf2 100755 --- a/tools/gen-feeds.py +++ b/scripts/gen-feeds.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 """Writes /llms.txt and the RSS feed of the thoughts (/thoughts/feed.xml) from the pages of the site. - python3 tools/gen-feeds.py [] + python3 scripts/gen-feeds.py [] -The folder defaults to the repository root. tools/deploy.sh runs it on the published copy, so both files always +The folder defaults to the repository root. scripts/deploy.sh runs it on the published copy, so both files always match the pages and nobody edits them by hand. Everything comes from each page's : , the meta description, the canonical URL and article:published_time. A page is listed in its section by its folder: projects/ (games), thoughts/<slug>/ (thoughts), research/ (write-ups). Standard library only. @@ -99,7 +99,7 @@ def when(p): def build(root): - """The text of each generated file for the site in `root`. The review server (tools/review/devserver.py) uses it.""" + """The text of each generated file for the site in `root`. The review server (scripts/review/devserver.py) uses it.""" global ROOT ROOT = os.path.abspath(root) return {path: make() for path, make in FILES.items()} diff --git a/tools/preview-artifact.py b/scripts/preview-artifact.py similarity index 98% rename from tools/preview-artifact.py rename to scripts/preview-artifact.py index dffe8f9..36ce36f 100644 --- a/tools/preview-artifact.py +++ b/scripts/preview-artifact.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Builds a preview of one page of the site as a Claude artifact bundle, so Tim can review it before it is published. - python3 tools/preview-artifact.py thoughts/humans-md [<out dir>] + python3 scripts/preview-artifact.py thoughts/humans-md [<out dir>] The out dir defaults to .preview/<page> (git ignores it). It holds index.html and the files it needs, at the paths the page uses: diff --git a/tools/reels/build.py b/scripts/reels/build.py similarity index 98% rename from tools/reels/build.py rename to scripts/reels/build.py index 5411395..f8b746d 100644 --- a/tools/reels/build.py +++ b/scripts/reels/build.py @@ -169,7 +169,7 @@ def loops(out, marks): def timeline(): """The clips of the development timelines: 640x360, silent, a poster at 60 %. - Kleinbahn from the frames of tools/reels/timeline.mjs, Wortzauber from the recordings of tools/reels/timeline-wz.mjs.""" + Kleinbahn from the frames of scripts/reels/timeline.mjs, Wortzauber from the recordings of scripts/reels/timeline-wz.mjs.""" out = os.path.join(SITE, 'assets', 'projects', 'timeline') os.makedirs(out, exist_ok=True) enc = ['-c:v', 'libx264', '-crf', '27', '-preset', 'slow', '-movflags', '+faststart', '-an'] diff --git a/tools/reels/fonts/OFL.txt b/scripts/reels/fonts/OFL.txt similarity index 100% rename from tools/reels/fonts/OFL.txt rename to scripts/reels/fonts/OFL.txt diff --git a/tools/reels/fonts/fredoka-500.woff2 b/scripts/reels/fonts/fredoka-500.woff2 similarity index 100% rename from tools/reels/fonts/fredoka-500.woff2 rename to scripts/reels/fonts/fredoka-500.woff2 diff --git a/tools/reels/fonts/fredoka-600.woff2 b/scripts/reels/fonts/fredoka-600.woff2 similarity index 100% rename from tools/reels/fonts/fredoka-600.woff2 rename to scripts/reels/fonts/fredoka-600.woff2 diff --git a/tools/reels/fonts/nunito-700.woff2 b/scripts/reels/fonts/nunito-700.woff2 similarity index 100% rename from tools/reels/fonts/nunito-700.woff2 rename to scripts/reels/fonts/nunito-700.woff2 diff --git a/tools/reels/kleinbahn-shots.mjs b/scripts/reels/kleinbahn-shots.mjs similarity index 98% rename from tools/reels/kleinbahn-shots.mjs rename to scripts/reels/kleinbahn-shots.mjs index 33e34bb..c30adf2 100644 --- a/tools/reels/kleinbahn-shots.mjs +++ b/scripts/reels/kleinbahn-shots.mjs @@ -1,5 +1,5 @@ // Renders the Kleinbahn reel shots frame by frame on the game's virtual clock. -// node tools/reels/kleinbahn-shots.mjs <path to tiny-train> [--test] [--only a-wide,b-train] +// node scripts/reels/kleinbahn-shots.mjs <path to tiny-train> [--test] [--only a-wide,b-train] // Frames land in <work>/kb/frames/<shot>/ (or <work>/kb/test/ with --test: three frames per shot). import fs from 'node:fs'; import path from 'node:path'; diff --git a/tools/reels/overlays.mjs b/scripts/reels/overlays.mjs similarity index 98% rename from tools/reels/overlays.mjs rename to scripts/reels/overlays.mjs index 152a1ee..201d523 100644 --- a/tools/reels/overlays.mjs +++ b/scripts/reels/overlays.mjs @@ -1,5 +1,5 @@ // Renders the transparent 1280x720 title, caption and end-card overlays of both reels into <work>/ov/kb and <work>/ov/wz. -// node tools/reels/overlays.mjs <path to wortzauber> (the Andika font comes from the game) +// node scripts/reels/overlays.mjs <path to wortzauber> (the Andika font comes from the game) // The caption texts live here: edit KB.items and WZ.items. import fs from 'node:fs'; import path from 'node:path'; diff --git a/tools/reels/pw.mjs b/scripts/reels/pw.mjs similarity index 100% rename from tools/reels/pw.mjs rename to scripts/reels/pw.mjs diff --git a/tools/reels/timeline-kleinbahn.mjs b/scripts/reels/timeline-kleinbahn.mjs similarity index 98% rename from tools/reels/timeline-kleinbahn.mjs rename to scripts/reels/timeline-kleinbahn.mjs index ea61cfd..2f27cb3 100644 --- a/tools/reels/timeline-kleinbahn.mjs +++ b/scripts/reels/timeline-kleinbahn.mjs @@ -1,5 +1,5 @@ // The development timeline of Kleinbahn: one stage per merged pull request that changed the game. -// Each stage is filmed at `ref` (the merge commit) by tools/reels/timeline.mjs. +// Each stage is filmed at `ref` (the merge commit) by scripts/reels/timeline.mjs. // setup, after: game code run once (G = the game's scope through __G, T = the timeline helpers) // until: an expression of G and T; the clock runs without drawing until it is true // each: code run before every frame (f = frame number, u = 0..1, e = eased u) diff --git a/tools/reels/timeline-wortzauber.mjs b/scripts/reels/timeline-wortzauber.mjs similarity index 99% rename from tools/reels/timeline-wortzauber.mjs rename to scripts/reels/timeline-wortzauber.mjs index bcddd3b..6fe3b97 100644 --- a/tools/reels/timeline-wortzauber.mjs +++ b/scripts/reels/timeline-wortzauber.mjs @@ -1,5 +1,5 @@ // The development timeline of Wortzauber: one stage per merged pull request that changed play, filmed in real time by -// tools/reels/timeline-wz.mjs at `ref` (the merge commit). +// scripts/reels/timeline-wz.mjs at `ref` (the merge commit). // scene: the name of a layout-check scene of the game (.claude/skills/checking-device-layouts/scenes/<name>.js), // read from the worktree at `ref`, so it is that version's own scene // setup: an async function (source) run in the page instead of a scene diff --git a/tools/reels/timeline-wz.mjs b/scripts/reels/timeline-wz.mjs similarity index 94% rename from tools/reels/timeline-wz.mjs rename to scripts/reels/timeline-wz.mjs index 5b28c90..41e112a 100644 --- a/tools/reels/timeline-wz.mjs +++ b/scripts/reels/timeline-wz.mjs @@ -1,9 +1,9 @@ // Films the development timeline of Wortzauber: one short screen recording per merged pull request, each at its own git state. -// node tools/reels/timeline-wz.mjs <path to wortzauber> [--dry] [--only pr65,pr63] -// For each stage of tools/reels/timeline-wortzauber.mjs it checks out the stage's commit as a git worktree, serves it on its +// node scripts/reels/timeline-wz.mjs <path to wortzauber> [--dry] [--only pr65,pr63] +// For each stage of scripts/reels/timeline-wortzauber.mjs it checks out the stage's commit as a git worktree, serves it on its // own port (a fresh browser each time, so no stale cache), opens /?dev=1&fresh=1 at 1280x720 in German, runs the stage's // setup and holds. The video and the cut points land in <work>/tl/wz/<id>/ (video.webm, marks.json). -// --dry saves one still per stage instead (<work>/tl/wz-dry/<id>.png). Then `python3 tools/reels/build.py timeline`. +// --dry saves one still per stage instead (<work>/tl/wz-dry/<id>.png). Then `python3 scripts/reels/build.py timeline`. import fs from 'node:fs'; import path from 'node:path'; import { execFileSync, spawn } from 'node:child_process'; diff --git a/tools/reels/timeline.mjs b/scripts/reels/timeline.mjs similarity index 95% rename from tools/reels/timeline.mjs rename to scripts/reels/timeline.mjs index d5be2ea..0db1af1 100644 --- a/tools/reels/timeline.mjs +++ b/scripts/reels/timeline.mjs @@ -1,9 +1,9 @@ // Films the development timeline of a game: one short clip per merged pull request, each at its own git state. -// node tools/reels/timeline.mjs <path to tiny-train> [--test] [--only pr13,pr11] -// For each stage of tools/reels/timeline-kleinbahn.mjs it checks out the stage's commit as a git worktree +// node scripts/reels/timeline.mjs <path to tiny-train> [--test] [--only pr13,pr11] +// For each stage of scripts/reels/timeline-kleinbahn.mjs it checks out the stage's commit as a git worktree // (node_modules linked from the repo), opens that version of the game on its virtual clock, renders the frames // and removes the worktree again. Frames land in <work>/tl/kb/<id>/ (<work>/tl/kb-test/ with --test: three frames). -// Then `python3 tools/reels/build.py timeline` cuts the clips into assets/projects/timeline/. +// Then `python3 scripts/reels/build.py timeline` cuts the clips into assets/projects/timeline/. import fs from 'node:fs'; import path from 'node:path'; import { execFileSync } from 'node:child_process'; diff --git a/tools/reels/wortzauber-takes.mjs b/scripts/reels/wortzauber-takes.mjs similarity index 98% rename from tools/reels/wortzauber-takes.mjs rename to scripts/reels/wortzauber-takes.mjs index 2c8602d..7364a04 100644 --- a/tools/reels/wortzauber-takes.mjs +++ b/scripts/reels/wortzauber-takes.mjs @@ -1,5 +1,5 @@ // Films the Wortzauber takes in real time. The dev server of the game must run (python3 devserver.py). -// node tools/reels/wortzauber-takes.mjs <path to wortzauber> [--rec] [--only meadow,quest] +// node scripts/reels/wortzauber-takes.mjs <path to wortzauber> [--rec] [--only meadow,quest] // Without --rec it only saves a still at each take's hold point, in <work>/wz/dry/. // With --rec the videos land in <work>/wz/rec/v-<take>/ and the cut points in <work>/wz/rec/marks.json. import fs from 'node:fs'; diff --git a/tools/review/devserver.py b/scripts/review/devserver.py similarity index 97% rename from tools/review/devserver.py rename to scripts/review/devserver.py index c3a120d..239ef3b 100644 --- a/tools/review/devserver.py +++ b/scripts/review/devserver.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Review server for the site: live reload and notes. Only the review site uses it. GitHub Pages serves t7d.dev. - python3 tools/review/devserver.py [--root .] [--notes .review/notes] [--host 127.0.0.1] [--port 8000] + python3 scripts/review/devserver.py [--root .] [--notes .review/notes] [--host 127.0.0.1] [--port 8000] - Serves the repository root with Cache-Control: no-cache. A missing page gets /404.html with status 404. - Every HTML page gets /__review/review.css and /__review/review.js. The page CSP gets connect-src 'self', so the @@ -12,7 +12,7 @@ - /__review/api/notes: GET lists the notes, open and done. POST writes one note as <notes>/<id>.json. The POST needs the header X-T7d-Review and a JSON body: a plain form from another site cannot send either. A handled note goes to <notes>/done/ with an "answer" field. The page shows the answer under "My notes". -- /llms.txt, /thoughts/feed.xml and /blog/feed.xml are made on each request by tools/gen-feeds.py, as the deploy makes them for gh-pages. +- /llms.txt, /thoughts/feed.xml and /blog/feed.xml are made on each request by scripts/gen-feeds.py, as the deploy makes them for gh-pages. - /.git, /.review, /.reels and /node_modules are not served. """ import argparse @@ -37,7 +37,7 @@ def gen_feeds(): - """tools/gen-feeds.py as a module (its file name has a hyphen).""" + """scripts/gen-feeds.py as a module (its file name has a hyphen).""" spec = importlib.util.spec_from_file_location("gen_feeds", os.path.join(HERE, "..", "gen-feeds.py")) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) diff --git a/tools/review/review.css b/scripts/review/review.css similarity index 96% rename from tools/review/review.css rename to scripts/review/review.css index fd77e6a..e04788a 100644 --- a/tools/review/review.css +++ b/scripts/review/review.css @@ -1,4 +1,4 @@ -/* The note tab of the review site (tools/review/devserver.py). Black and white, as the site (DESIGN.md). */ +/* The note tab of the review site (scripts/review/devserver.py). Black and white, as the site (DESIGN.md). */ .rv-tab { position: fixed; right: 0; top: 50%; z-index: 2147483000; transform: translateY(-50%) rotate(180deg); writing-mode: vertical-rl; diff --git a/tools/review/review.js b/scripts/review/review.js similarity index 98% rename from tools/review/review.js rename to scripts/review/review.js index 2ce16ed..426b4d9 100644 --- a/tools/review/review.js +++ b/scripts/review/review.js @@ -1,4 +1,4 @@ -/* The review site (tools/review/devserver.py): live reload and the note tab. Never on t7d.dev. */ +/* The review site (scripts/review/devserver.py): live reload and the note tab. Never on t7d.dev. */ (function () { 'use strict'; var API = '/__review/api/'; diff --git a/tools/review/show.py b/scripts/review/show.py similarity index 97% rename from tools/review/show.py rename to scripts/review/show.py index 197b58e..a8db480 100644 --- a/tools/review/show.py +++ b/scripts/review/show.py @@ -2,7 +2,7 @@ """Show where a review note points: open the page at the width and in the theme of the note, frame the part in red, and save <notes>/<id>.png next to the note (it stays out of git). - uv run --no-project --with playwright python3 tools/review/show.py <id> [<id> ...] [--suffix after] + uv run --no-project --with playwright python3 scripts/review/show.py <id> [<id> ...] [--suffix after] --suffix after writes <id>.after.png: run it after the fix and compare the two pictures. The script serves the working tree itself (devserver.py on a free port), so it shows the files as they are now. diff --git a/tools/review/test_devserver.py b/scripts/review/test_devserver.py similarity index 98% rename from tools/review/test_devserver.py rename to scripts/review/test_devserver.py index cffb4cd..2103617 100644 --- a/tools/review/test_devserver.py +++ b/scripts/review/test_devserver.py @@ -1,4 +1,4 @@ -"""Tests of the review server. Run: python3 -m unittest tools/review/test_devserver.py""" +"""Tests of the review server. Run: python3 -m unittest scripts/review/test_devserver.py""" import json import os import sys diff --git a/tools/ship.sh b/scripts/ship.sh similarity index 93% rename from tools/ship.sh rename to scripts/ship.sh index a83dc73..9906216 100755 --- a/tools/ship.sh +++ b/scripts/ship.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash # Ship a pull request in one step: merge it, wait for the deploy, and check that t7d.dev serves the new files. -# tools/ship.sh [<pr>] the PR number. Without it: the PR of the current branch. +# scripts/ship.sh [<pr>] the PR number. Without it: the PR of the current branch. # 1. gh pr merge --merge --delete-branch. A PR that is already merged is not merged again, so a second run # after a failed step does the other steps only. # 2. This clone goes to master (fast-forward only), so the review site shows master. It stops on changes to tracked files. # 3. The workflow "Deploy to gh-pages" of the merge commit must pass. It writes the commit "Deploy <sha>" on gh-pages, -# unless the PR changed no site file (tools/, docs): then the script ends here. +# unless the PR changed no site file (scripts/, docs): then the script ends here. # 4. The GitHub Pages build of the newest gh-pages commit must pass (a later deploy contains this one). # 5. Each site file of the PR must be on t7d.dev as it is on gh-pages now (a query string skips the CDN cache). set -euo pipefail @@ -59,7 +59,7 @@ fi wait_run pages-build-deployment "$(git rev-parse origin/gh-pages)" echo "== $SITE_URL serves the new files" -read -r -a site <<<"$(sed -n 's/^SITE=(\(.*\))$/\1/p' tools/deploy.sh)" +read -r -a site <<<"$(sed -n 's/^SITE=(\(.*\))$/\1/p' scripts/deploy.sh)" mapfile -t files < <(git diff --name-only --diff-filter=AM "$sha^1" "$sha" -- "${site[@]}") bad=0 for f in "${files[@]}"; do From 9b70de361c40bdd38815ecb1e2a8d2ace282f7f3 Mon Sep 17 00:00:00 2001 From: Tim Conrad <4759569+Tim-Conrad@users.noreply.github.com> Date: Tue, 10 Nov 2026 23:39:06 +0100 Subject: [PATCH 2/4] Tools sheet and the papercuts page, README rendered at deploy Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KFxuEteUiyeGMzLJ2VGnGc --- .github/workflows/check.yml | 4 ++ .github/workflows/deploy.yml | 3 + CLAUDE.md | 1 + README.md | 6 +- assets/site.css | 4 ++ index.html | 49 +++++++++++++-- scripts/deploy.sh | 4 +- scripts/gen-tools.py | 108 +++++++++++++++++++++++++++++++++ scripts/test_gen_tools.py | 113 +++++++++++++++++++++++++++++++++++ sitemap.xml | 2 + tools/index.html | 68 +++++++++++++++++++++ tools/papercuts/index.html | 76 +++++++++++++++++++++++ 12 files changed, 429 insertions(+), 9 deletions(-) create mode 100644 scripts/gen-tools.py create mode 100644 scripts/test_gen_tools.py create mode 100644 tools/index.html create mode 100644 tools/papercuts/index.html diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 56df3e2..6a93c9c 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -13,3 +13,7 @@ jobs: - uses: actions/checkout@v4 - name: CSP rules and sitemap run: python3 scripts/check.py + - name: Tests of the tool page generator + run: | + pip install markdown==3.11.3 + python3 -m unittest scripts/test_gen_tools.py diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bdb7500..04ae5ea 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -19,8 +19,11 @@ jobs: - uses: actions/checkout@v4 - name: Check the pages run: python3 scripts/check.py + - name: Install the Markdown renderer for the tool pages + run: pip install markdown==3.11.3 - name: Publish the site files to gh-pages env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GIT_AUTHOR_NAME: github-actions[bot] GIT_AUTHOR_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com run: scripts/deploy.sh diff --git a/CLAUDE.md b/CLAUDE.md index 19044b4..689bd72 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,6 +13,7 @@ t7d.dev: the personal site of Tim Conrad. Plain HTML, CSS and a little JavaScrip - Read `DESIGN.md` before you change a page, a style or any wording. The two rules that matter most: the site is black and white with colour only in pictures and videos, and the wording is plain with no slogans or overstatement. - Claim only what is true. Do not invent facts about Tim, the research or the games. - Do not rewrite the text of the write-up under `research/`. It is Tim's own. +- A new tool page: copy `tools/papercuts/index.html`, keep its `<!-- tool-readme: owner/name -->` placeholder, and add the tool to `tools/index.html`, the Tools section of `index.html` and `sitemap.xml`. `scripts/gen-tools.py` puts the README of the latest release into the page at deploy time. Never paste a README by hand. Test: `uv run --no-project --with markdown==3.11.3 python3 -m unittest scripts/test_gen_tools.py`. - A new or revised thought: use the skill `adding-a-thought`. `llms.txt` and the feeds are made at deploy time (`scripts/gen-feeds.py`); do not commit them. - Keep the Content-Security-Policy of each page strict: no inline scripts or styles, no `style=""` attributes, and no request to another site on page load. `assets/theme.js` and `assets/site.js` are the only scripts. - `master` is the source. Never edit `gh-pages` by hand: the workflow publishes it after every push to `master`, and the sealed admin app (`Tim-Conrad/sealed`) publishes encrypted sites to `sealed/` on it, a folder that exists only there. `scripts/deploy.sh` must keep `sealed/`. diff --git a/README.md b/README.md index 36d24cf..088f814 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ It is plain HTML, CSS and a little JavaScript. There is no build step and no dep | Thoughts | `/thoughts/` | Ideas I work through and come back to; each in `/thoughts/<slug>/` with a status, sources, revisions and feedback | | Thought | `/thoughts/humans-md/` | A humans.md for working with models (animated diagrams, the file `HUMANS.md` to download) | | Thought | `/thoughts/encrypted-patches/` | Encrypted patches, unlocked on patch day (inline SVG diagrams) | +| Tools | `/tools/` | Tools I published; each in `/tools/<slug>/` with an intro and the README of its latest release | +| Tool | `/tools/papercuts/` | Papercuts: one place where my agents and I report friction | | Write-up | `/research/cve-2022-46166/` | CVE-2022-46166: template injection in Spring Boot Admin | | Kleinbahn | `/projects/kleinbahn/` | Reel, live demo from `tiny-train.beacon.host`, features, stills | | Wortzauber | `/projects/wortzauber/` | Reel, live demo from `wortzauber.beacon.host`, features, stills | @@ -27,7 +29,7 @@ It is plain HTML, CSS and a little JavaScript. There is no build step and no dep | Path | What it is | | --- | --- | | `index.html`, `404.html` | Front page and not-found page | -| `research/`, `projects/`, `thoughts/` | One folder with an `index.html` per page | +| `research/`, `projects/`, `thoughts/`, `tools/` | One folder with an `index.html` per page | | `blog/`, `CVE/` | Redirects from old addresses | | `assets/site.css` | All styles and the colour tokens of both themes | | `assets/theme.js` | Applies the saved theme before the first paint | @@ -81,7 +83,7 @@ GitHub Pages serves the `gh-pages` branch at <https://t7d.dev>. `master` is the 1. Merge a change into `master`. 2. The workflow `.github/workflows/deploy.yml` runs `scripts/deploy.sh`. -3. The script copies the site files of that commit to `gh-pages`, writes `llms.txt` and the feeds from the pages with `scripts/gen-feeds.py`, and pushes. `scripts/`, `.claude/`, `.github/` and this README stay on `master`. +3. The script copies the site files of that commit to `gh-pages`, writes `llms.txt` and the feeds from the pages with `scripts/gen-feeds.py`, puts the README of each tool's latest release into its page with `scripts/gen-tools.py`, and pushes. `scripts/`, `.claude/`, `.github/` and this README stay on `master`. To ship a pull request in one step, run `scripts/ship.sh [<pr>]`. It merges the PR, puts this clone on `master`, waits for the deploy and the Pages build, and checks that t7d.dev serves each site file of the PR. It runs no checks: look at the pages first. diff --git a/assets/site.css b/assets/site.css index b42d33e..9dd1f13 100644 --- a/assets/site.css +++ b/assets/site.css @@ -279,6 +279,10 @@ code, kbd, pre { font-family: var(--mono); font-size: .88em; } .prose li::marker { font-family: var(--mono); font-size: .85em; color: var(--muted); } .prose :not(pre) > code { background: var(--surface); border: 1px solid var(--soft); padding: .08em .35em; overflow-wrap: anywhere; } .prose a { overflow-wrap: anywhere; } +.tbl { overflow-x: auto; min-width: 0; } +.tbl table { border-collapse: collapse; width: 100%; font-size: var(--step--1); } +.tbl th, .tbl td { border: 1px solid var(--soft); padding: .4rem .6rem; text-align: left; vertical-align: top; } +.tbl th { border-bottom: var(--rule) solid var(--ink); font-family: var(--mono); font-weight: 500; } .note { border: var(--rule) solid var(--ink); background: var(--surface); padding: 1rem 1.15rem; display: grid; gap: .4rem; } .payload { background: var(--code-bg); color: var(--code-fg); border: 1px solid var(--code-line); } .payload dl { margin: 0; display: grid; } diff --git a/index.html b/index.html index a49b152..416c3cb 100644 --- a/index.html +++ b/index.html @@ -5,11 +5,11 @@ <meta name="viewport" content="width=device-width, initial-scale=1"> <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'self'; script-src 'self'; img-src 'self'; font-src 'self'; media-src 'self'; base-uri 'none'; form-action 'none'"> <title>Tim Conrad - + - + @@ -43,11 +43,11 @@

    Sheet 1 · Overview

    Tim Conrad

    -

    I do security research and build browser games. This site holds two games, the ideas I am working through, one vulnerability write-up and my PGP key.

    +

    I do security research and build browser games. This site holds two games, the ideas I am working through, one vulnerability write-up, the tools I published and my PGP key.

    Site
    t7d.dev
    -
    Contents
    2 games, 2 thoughts, 1 write-up
    +
    Contents
    2 games, 2 thoughts, 1 write-up, 1 tool
    PGP fingerprint
    A886 6CB1 6B07 9685 5615
    74B2 A91D ABBE C996 7390
    @@ -161,10 +161,47 @@

    Remote code execution in Spring Boot Admin

    +
    + +
    +
    -

    Sheet 5 · PGP key

    +

    Sheet 6 · PGP key

    PGP key

    Use this key to verify anything published under my name. Compare the fingerprint before you trust the key.

    @@ -190,7 +227,7 @@

    PGP key

    -

    Sheet 6 · Contact

    +

    Sheet 7 · Contact

    Contact

    Message me on LinkedIn. My code is on GitHub.

    diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 512cdec..1e70834 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -7,7 +7,7 @@ # encrypted sites there, one folder per site (sealed//). This script keeps that folder as it is. set -euo pipefail -SITE=(index.html 404.html CNAME .nojekyll key.asc robots.txt sitemap.xml assets thoughts blog research projects CVE) +SITE=(index.html 404.html CNAME .nojekyll key.asc robots.txt sitemap.xml assets thoughts tools blog research projects CVE) root=$(git rev-parse --show-toplevel) src=$(git -C "$root" rev-parse HEAD) @@ -23,6 +23,8 @@ fi git -C "$root" archive "$src" "${SITE[@]}" | tar -x # llms.txt and the RSS feed of the thoughts are made from the published pages, never by hand python3 "$root/scripts/gen-feeds.py" "$tree" +# each tool page shows the README of the latest release of its repo; a failed fetch stops the deploy +python3 "$root/scripts/gen-tools.py" "$tree" git add -A if git diff --cached --quiet; then diff --git a/scripts/gen-tools.py b/scripts/gen-tools.py new file mode 100644 index 0000000..2fe702b --- /dev/null +++ b/scripts/gen-tools.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""Puts the README of each tool into its page under tools//, at deploy time. + + python3 scripts/gen-tools.py [] [--readme-dir ] + +A tool page holds a placeholder that names the repo: + + +

    Any text for the review site.

    + + +The script fetches the README of the repo at its latest release tag, renders it, and replaces the text between the +two comments. So the page always shows the README of the released tool, and nobody copies it by hand. +--readme-dir reads /.md instead (tests, and a look at the page before a release). + +Raw HTML in the README is shown as text, so that the page keeps its strict Content-Security-Policy. +If a README cannot be fetched, the script exits 1 and the deploy stops: an old README on the page is drift. +Needs Python-Markdown (the deploy workflow installs it). scripts/deploy.sh runs it on the published copy. +""" +import html, json, os, re, sys, urllib.request + +import markdown + +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +PLACEHOLDER = re.compile(r'()(.*?)()', re.S) + + +class Fail(Exception): + pass + + +def fetch(url): + req = urllib.request.Request(url, headers={'User-Agent': 't7d.dev-deploy', 'Accept': 'application/vnd.github+json'}) + token = os.environ.get('GITHUB_TOKEN') + if token and url.startswith('https://api.github.com/'): + req.add_header('Authorization', f'Bearer {token}') + try: + with urllib.request.urlopen(req, timeout=20) as r: + return r.read().decode('utf-8') + except Exception as e: + raise Fail(f'{url}: {e}') + + +def readme_from_github(repo): + tag = json.loads(fetch(f'https://api.github.com/repos/{repo}/releases/latest'))['tag_name'] + return fetch(f'https://raw.githubusercontent.com/{repo}/{tag}/README.md'), tag + + +def render(text): + text = re.sub(r'\A\s*# [^\n]*\n', '', text) # the page has its own h1 + md = markdown.Markdown(extensions=['fenced_code', 'tables', 'toc']) + # No raw HTML: the block and inline HTML handlers go, so markdown escapes it like text. + md.preprocessors.deregister('html_block') + md.inlinePatterns.deregister('html') + out = md.convert(text) + + def code(m): + lang = re.search(r'class="language-([\w-]+)"', m.group(1)) + bar = html.escape(lang.group(1)) if lang else 'text' + return f'
    {bar}
    {m.group(1)}
    ' + + out = re.sub(r'
    (.*?)
    ', code, out, flags=re.S) + out = re.sub(r'(.*?)
    ', r'
    \1
    ', out, flags=re.S) + return out + + +def main(argv): + args = list(argv) + readme_dir = None + if '--readme-dir' in args: + i = args.index('--readme-dir') + readme_dir = args[i + 1] + del args[i:i + 2] + root = os.path.abspath(args[0]) if args else ROOT + tools = os.path.join(root, 'tools') + if not os.path.isdir(tools): + return 0 + for slug in sorted(os.listdir(tools)): + page = os.path.join(tools, slug, 'index.html') + if not os.path.isfile(page): + continue + s = open(page, encoding='utf-8').read() + m = PLACEHOLDER.search(s) + if not m: + continue + repo = m.group(2) + if readme_dir: + path = os.path.join(readme_dir, f'{slug}.md') + if not os.path.isfile(path): + raise Fail(f'{path} does not exist') + text, source = open(path, encoding='utf-8').read(), 'local copy' + else: + text, source = readme_from_github(repo) + body = render(text) + new = f'{m.group(1)}\n\n{body}\n{m.group(4)}' + s = s[:m.start()] + new + s[m.end():] + with open(page, 'w', encoding='utf-8') as f: + f.write(s) + print(f'tools/{slug}: README of {repo} ({source})') + return 0 + + +if __name__ == '__main__': + try: + sys.exit(main(sys.argv[1:])) + except Fail as e: + print(f'error: {e}') + sys.exit(1) diff --git a/scripts/test_gen_tools.py b/scripts/test_gen_tools.py new file mode 100644 index 0000000..406d0d9 --- /dev/null +++ b/scripts/test_gen_tools.py @@ -0,0 +1,113 @@ +"""Tests of gen-tools.py. Run: uv run --no-project --with markdown==3.11.3 python3 -m unittest scripts/test_gen_tools.py""" +import os, shutil, subprocess, sys, tempfile, unittest + +HERE = os.path.dirname(os.path.abspath(__file__)) + +PAGE = """X
    + +

    The README is added when the site is published.

    + +
    """ + +README = """# Papercuts + +Papercuts is one place. + +## Quick start + +See [the relay](#run-the-relay). + +```sh +npx papercuts add tool "x " +``` + +| A | B | +| --- | --- | +| 1 | 2 | + +## Run the relay + + + +Text with raw html inline. +""" + + +class GenTools(unittest.TestCase): + def setUp(self): + self.site = tempfile.mkdtemp() + self.readmes = tempfile.mkdtemp() + os.makedirs(os.path.join(self.site, 'tools', 'papercuts')) + with open(os.path.join(self.site, 'tools', 'papercuts', 'index.html'), 'w') as f: + f.write(PAGE) + with open(os.path.join(self.readmes, 'papercuts.md'), 'w') as f: + f.write(README) + + def tearDown(self): + shutil.rmtree(self.site) + shutil.rmtree(self.readmes) + + def page(self): + with open(os.path.join(self.site, 'tools', 'papercuts', 'index.html')) as f: + return f.read() + + def run_gen(self): + return subprocess.run([sys.executable, os.path.join(HERE, 'gen-tools.py'), self.site, '--readme-dir', self.readmes], + capture_output=True, text=True) + + def test_fills_readme_placeholder(self): + r = self.run_gen() + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + s = self.page() + self.assertNotIn('added when the site is published', s) + self.assertIn('

    Quick start

    ', s) + self.assertIn('href="#run-the-relay"', s) + self.assertIn('id="run-the-relay"', s) + self.assertIn('', s) + self.assertIn('', s) + + def test_drops_the_title(self): + self.run_gen() + self.assertNotIn('
    sh
    ', s)
    +        self.assertIn('<y>', s)
    +
    +    def test_table_scrolls_in_own_box(self):
    +        self.run_gen()
    +        self.assertIn('
    ', self.page()) + + def test_strips_raw_html_and_script(self): + self.run_gen() + s = self.page() + self.assertNotIn(' + + + + + + + +
    + +
    + ← Home +

    Tools

    +

    Small tools I built for my own work and published. Each page shows the README of the latest release.

    +
    + +
    + +
    +
    + +
    +
    + Tim Conrad + No cookies, no trackers, no third-party requests +
    +
    + + + diff --git a/tools/papercuts/index.html b/tools/papercuts/index.html new file mode 100644 index 0000000..e44a87f --- /dev/null +++ b/tools/papercuts/index.html @@ -0,0 +1,76 @@ + + + + + + +Papercuts · Tim Conrad + + + + + + + + + + + + + + + + + + + +
    + +
    + ← Tools +

    Papercuts

    +

    One place where my coding agents and I report friction. Each report becomes a GitHub issue in the repo of the project.

    +

    Tim Conrad · Coding agents · Feedback · Node.js

    +
    +
    Version
    v0.1.0
    +
    License
    MIT
    + +
    +
    + +
    +
    +

    Why I built it

    +

    Small things get in the way all day. A button overlaps on the phone. A tool prints output that my agent cannot use. A rule in a project file points to a file that moved. Each one costs a few minutes, and the next session pays them again.

    +

    I had built the same fix three times: a note tab on my pen-and-paper review site, a note tab on the review copy of this site, and a log where agents wrote down what slowed them down. Each kept its notes in its own place and its own shape.

    +

    Papercuts puts them together. I report on the page, my agent reports from the terminal or from the browser, and every report becomes a GitHub issue in the same format. The agent that picks up the work reads all of them the same way, and fixes them one by one.

    +

    It runs only on my local sites, behind my login. Visitors of a public site never see it.

    + +

    The README of the latest release goes here when the site is published. Until then, read it on GitHub.

    + +
    +
    + +
    + +
    +
    + Tim Conrad + No cookies, no trackers, no third-party requests +
    +
    + + + From 5aba66cbb285a7db3444c4c3119804b24b1ea8e6 Mon Sep 17 00:00:00 2001 From: Tim Conrad <4759569+Tim-Conrad@users.noreply.github.com> Date: Tue, 10 Nov 2026 23:40:03 +0100 Subject: [PATCH 3/4] Local drafts folder, never published Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KFxuEteUiyeGMzLJ2VGnGc --- .gitignore | 3 +++ CLAUDE.md | 1 + 2 files changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index d6bfe8a..53916dc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ __pycache__/ .review/ .env .preview/ + +# local drafts: never committed, never published (the repo is public) +/drafts/ diff --git a/CLAUDE.md b/CLAUDE.md index 689bd72..27b526d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,6 +14,7 @@ t7d.dev: the personal site of Tim Conrad. Plain HTML, CSS and a little JavaScrip - Claim only what is true. Do not invent facts about Tim, the research or the games. - Do not rewrite the text of the write-up under `research/`. It is Tim's own. - A new tool page: copy `tools/papercuts/index.html`, keep its `` placeholder, and add the tool to `tools/index.html`, the Tools section of `index.html` and `sitemap.xml`. `scripts/gen-tools.py` puts the README of the latest release into the page at deploy time. Never paste a README by hand. Test: `uv run --no-project --with markdown==3.11.3 python3 -m unittest scripts/test_gen_tools.py`. +- `drafts/` holds local drafts. Git ignores it, and it is not in `SITE` or `sitemap.xml`. The repo is public, so a draft never goes into git. To publish a draft, move it to its real folder. - A new or revised thought: use the skill `adding-a-thought`. `llms.txt` and the feeds are made at deploy time (`scripts/gen-feeds.py`); do not commit them. - Keep the Content-Security-Policy of each page strict: no inline scripts or styles, no `style=""` attributes, and no request to another site on page load. `assets/theme.js` and `assets/site.js` are the only scripts. - `master` is the source. Never edit `gh-pages` by hand: the workflow publishes it after every push to `master`, and the sealed admin app (`Tim-Conrad/sealed`) publishes encrypted sites to `sealed/` on it, a folder that exists only there. `scripts/deploy.sh` must keep `sealed/`. From 070de61ccbd1f35aefebb34f7b10372398992520 Mon Sep 17 00:00:00 2001 From: Tim Conrad <4759569+Tim-Conrad@users.noreply.github.com> Date: Tue, 10 Nov 2026 23:45:21 +0100 Subject: [PATCH 4/4] CI: install Python-Markdown into a setup-python interpreter, not the system Python Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KFxuEteUiyeGMzLJ2VGnGc --- .github/workflows/check.yml | 3 +++ .github/workflows/deploy.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 6a93c9c..38b072a 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -13,6 +13,9 @@ jobs: - uses: actions/checkout@v4 - name: CSP rules and sitemap run: python3 scripts/check.py + - uses: actions/setup-python@v5 + with: + python-version: '3.12' - name: Tests of the tool page generator run: | pip install markdown==3.11.3 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 04ae5ea..bc1f2ba 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -19,6 +19,9 @@ jobs: - uses: actions/checkout@v4 - name: Check the pages run: python3 scripts/check.py + - uses: actions/setup-python@v5 + with: + python-version: '3.12' - name: Install the Markdown renderer for the tool pages run: pip install markdown==3.11.3 - name: Publish the site files to gh-pages