fuzzyworld-tools / files / PLAN.md
PLAN.md
1758548c 2026-10-27 Pavel Safronov | 1 | # Plan |
| 2 | ||
| 3 | Where fuzzyworld-tools goes from here: from "a static mirror of my repos" to a small, open-source | |
| 4 | code host built on one rule: **DreamHost only serves files; other machines do the work.** | |
| 5 | ||
| 6 | This is a planning document. Nothing in it is promised; the order is the current priority. | |
a890b8a6 2026-11-04 Pavel Safronov | 7 | Last updated 2026-11-03. |
77346d8a 2026-10-29 Pavel Safronov | 8 | |
| 9 | **Who it is for.** First, me: I build the site I want and need (no GitLab, no WordPress, as few machines as possible). | |
| 10 | Second, anyone who wants to get going with the fewest resources. Anything that only makes sense for my setup is | |
| 11 | tagged ๐ **plugin**: it should be optional and separable, so that the core stays small enough for most people. | |
1758548c 2026-10-27 Pavel Safronov | 12 | |
| 13 | ## The machines | |
| 14 | ||
| 15 | | Machine | Does | Can't do | | |
| 16 | |---|---|---| | |
| 17 | | DreamHost (shared hosting) | serves static files with no rate limits; git over ssh; hourly cron; `post-receive` hooks | long-running processes, Chromium, V8's JIT (Node runs `--jitless`), native addons | | |
| 18 | | TrueNAS (home, always on) | Docker: CI runner, headless browsers, screenshots, code-server; already mirrors every repo | be reached from the internet. It **pulls** work from DreamHost over ssh instead | | |
| 19 | | Laptop | `fw deploy`, interactive work; can act as a second runner | stay on | | |
| 20 | ||
| 21 | Everything on the sites stays pre-generated: no app server, no database, no accounts, repo content is | |
| 22 | never executed. Where a feature needs JavaScript, it is one script **we** ship (CSP `script-src 'self'`, | |
| 23 | never anything from a repo). | |
| 24 | ||
| 25 | ## Now (prioritized) | |
| 26 | ||
9eb01df7 2026-10-27 Pavel Safronov | 27 | All done: โ live since 2026-10-27 (v1.1.0), unless noted. |
1758548c 2026-10-27 Pavel Safronov | 28 | |
392edf54 2026-11-04 Pavel Safronov | 29 | 1. โ Apps index: newest deploy first (2026-11-04: now newest code commit first, and both the code and apps indexes are tables, not tiles). (It once listed the commits of each deploy; removed 2026-10-27 as noise.) |
9eb01df7 2026-10-27 Pavel Safronov | 30 | 2. โ **LICENSE file** (MIT) in this repo. Each repo's license on its summary page. |
| 31 | 3. โ **Principles** in four places: `/why/`, `/how/`, a new `/principles/` page, and a "Philosophy" section in the | |
| 32 | README. `/how/` is still a draft that `install.sh` doesn't install (see Backlog); the principle is in it for | |
| 33 | when it goes live. | |
| 34 | 4. โ **File navigation** | |
1758548c 2026-10-27 Pavel Safronov | 35 | 1. "Open in VS Code" link on every summary page (`vscode://vscode.git/clone?url=โฆ`). |
| 36 | 2. Syntax highlighting at generation time (Pygments in `~/fw/pylib`; no JavaScript). | |
| 37 | 3. Per-file history, blame, and browsing every branch not yet merged into the default branch. | |
| 38 | 4. VS Code in the browser at `apps.fuzzyworld.net/vscode/?repo=<name>`: the `vscode-web` build plus a | |
| 39 | read-only file-system extension reading `files.json` and `objects/<sha>` from code.fuzzyworld.net. | |
9eb01df7 2026-10-27 Pavel Safronov | 40 | 5. โ **CI runner** on TrueNAS. See [CI](#ci). Verified live with nordic-skating and tower. |
| 41 | 6. โ **Screenshots** as a built-in runner job. See [Screenshots](#screenshots). Live for nordic-skating and tower. | |
1758548c 2026-10-27 Pavel Safronov | 42 | |
| 43 | ## Next (in this order) | |
| 44 | ||
| 45 | The order goes from cheap to expensive, and each step reuses what the one before it built. | |
| 46 | ||
77346d8a 2026-10-29 Pavel Safronov | 47 | 0. **Runners without TrueNAS** (decided 2026-10-28; see [Runners](#runners)): runner tags, the laptop as a runner when |
| 48 | idle and plugged in, secrets in a folder on each runner, and "no runners" as a normal state. Goes first. | |
73733e6e 2026-11-01 Pavel Safronov | 49 | **Built and committed 2026-10-31 (a499715); deployed 2026-10-31** (server, then the TrueNAS runner, then the laptop runner with `--authorize`, |
462e50c2 2026-10-31 Pavel Safronov | 50 | in that order: a new runner against the old server has every heartbeat refused). Smoke-tested: both runners poll |
| 51 | DreamHost and find nothing to run; no real job has gone through them yet. Review follow-ups (claim check on `done`, | |
0e603cf7 2026-10-31 Pavel Safronov | 52 | tolerant `release`) deployed the same day. |
1758548c 2026-10-27 Pavel Safronov | 53 | 1. **Feeds**: Atom per repo (commits, releases), for app deploys, and site-wide. Smallest step: the data is already |
77346d8a 2026-10-29 Pavel Safronov | 54 | gathered for the log pages and the apps index. The blog reuses it. |
| 55 | 1b. **Blog** at `blog.fuzzyworld.net` (see [Blog](#blog)): a generator that turns a folder or repo of Markdown into a | |
| 56 | blog from one JSON file, plus a one-time WordPress import. Replaces WordPress. | |
1758548c 2026-10-27 Pavel Safronov | 57 | 2. **Releases**: a page per tag (the tag's message as notes), `git archive` `.zip`/`.tar.gz`, and files the |
| 58 | runner builds (APKs, extensions). Import the release assets already in `~/code/gitlab-export/*/release-assets/`. | |
| 59 | Comes early because some of those files exist nowhere else. Adds a releases feed to step 1. | |
| 60 | 3. **Stats and heatmap**, built together, since both are computed from the same pass over the history when pages are generated: | |
| 61 | - stats: languages by bytes, size, commit count, share of commits made with AI (see [Provenance](#provenance)) | |
| 62 | - heatmap: a commit-activity SVG per repo and site-wide | |
| 63 | 4. **Wiki**: render a repo's `docs/` (or `wiki/`) folder with a sidebar. Import the GitLab wikis in | |
| 64 | `~/code/gitlab-export/*/wikis.json`. Static rendering only; editing comes with pull requests. | |
55e5fc3e 2026-11-04 Pavel Safronov | 65 | 5. **Issues** (built 2026-11-03): see [Issues](#issues). The first feature that reads email; builds the mailbox plumbing. |
77346d8a 2026-10-29 Pavel Safronov | 66 | 5b. **Blog comments** by email (see [Comments](#comments)): right after Issues, because it reuses the mailbox plumbing. |
1758548c 2026-10-27 Pavel Safronov | 67 | 6. **Pull requests**: see [Pull requests](#pull-requests). Last, because it reuses the issues mailbox plumbing |
| 68 | and needs the CI runner (approved CI runs on contributed code). | |
| 69 | ||
77346d8a 2026-10-29 Pavel Safronov | 70 | ## Runners |
| 71 | ||
| 72 | Goal (2026-10-28): TrueNAS becomes optional (๐ plugin), and the laptop can do the work. Running jobs needs no | |
| 73 | always-on machine; a small setup is a DreamHost account and a laptop. | |
| 74 | ||
| 75 | - **No runners is a normal state.** Jobs stay in `pending/` until a runner claims them (the queue is files, so this | |
a4997156 2026-10-31 Pavel Safronov | 76 | already works). The dashboard says "waiting for a runner" instead of looking broken (a runner counts as gone after |
| 77 | 15 minutes without a claim; each claim writes `~/fw/runners/<name>`). | |
| 78 | - **Laptop runner** (launchd via `runner/install-laptop.sh`, every 2 minutes), only when it will not hurt my work: on AC | |
| 79 | power, CPU at least half idle, Docker running, containers at a low CPU share, no new claims once I get busy (checked | |
| 80 | between jobs; a job already running is not stopped). Everything scheduled runs then. It assumes the laptop | |
77346d8a 2026-10-29 Pavel Safronov | 81 | is on a few hours a day; that is enough for me. |
a4997156 2026-10-31 Pavel Safronov | 82 | - **Lease.** A claimed job needs a heartbeat (`beat`, every 30 seconds, from a thread in the runner). A job whose runner |
| 83 | went quiet for ~2 minutes (lid closed) goes back to `pending/` instead of waiting 2 hours. If that runner wakes up and | |
462e50c2 2026-10-31 Pavel Safronov | 84 | finishes anyway, its results are uploaded too, and the job may have run twice: accepted. Its `done` is refused, |
| 85 | so it cannot remove the new holder's claim. Runners that send no | |
a4997156 2026-10-31 Pavel Safronov | 86 | heartbeat (old versions) keep the 2-hour rule. |
| 87 | - **Tags** (`docker`, `chrome`, `android`): job `tags:` are honoured, and screenshots need `chrome`. The runner learns what | |
| 88 | a pipeline needs only after cloning, so a runner that lacks a tag gives the job back (`release`); the server | |
| 89 | remembers and does not offer it to that runner again. Simple, at the cost of one clone per declined job. | |
77346d8a 2026-10-29 Pavel Safronov | 90 | - **Secrets.** Each runner has a folder (`โฆ/secrets/<repo>.env`; on TrueNAS `/mnt/main-2025/fw-runner/secrets/`). |
a4997156 2026-10-31 Pavel Safronov | 91 | A deploy job tagged `secrets` (a reserved tag: "this repo's secrets") runs only on a runner that has the file; no repo |
| 92 | uses it yet. Only default-branch and tag jobs get secrets. Put them on the laptop and TrueNAS is not needed. Laptop theft means rotating them (FileVault on). | |
77346d8a 2026-10-29 Pavel Safronov | 93 | - **Stretch goal: a secure secrets API** (๐): `runner-api` hands each job its secrets over ssh, per runner key, so |
| 94 | no runner stores them. Wait for user requests or downtime. | |
| 95 | ||
| 96 | ## Blog | |
| 97 | ||
| 98 | `blog.fuzzyworld.net` is WordPress today (theme Scrawl, 144 posts, 4 pages, 45 comments, 15 media files, 3 categories, | |
| 99 | 16 tags, one author). Goal: the same site, generated by this repo, with no WordPress. | |
| 100 | ||
0e603cf7 2026-10-31 Pavel Safronov | 101 | **Built and live 2026-10-31 at `writing.fuzzyworld.net`** (queue item Q4; not linked from anywhere yet). The generator is |
| 102 | `server/blog.py` (+ `server/atom.py`, `server/site-blog/`), the importer `lib/wp_import.py`; commands and behavior are in | |
bb7e1f57 2026-11-06 Pavel Safronov | 103 | docs/blog.md. Decisions taken: build on the laptop from Dropbox and upload only the audited output folder (never |
0e603cf7 2026-10-31 Pavel Safronov | 104 | the source); footer "All rights reserved" notice with an AI-training line and a `robots.txt` blocking AI crawlers; the |
| 105 | WordPress text wins when a source file differs (similarity under 0.95: 100 of the 142 posts are written from WordPress | |
| 106 | into `~/Blog/wordpress/`); the old site's custom CSS is carried over (hidden `<br>`, dotted scene breaks, underlined | |
| 107 | chapter headings); a light/dark switch and reading-progress bar added (they needed `script-src 'self'`). The Atom | |
73733e6e 2026-11-01 Pavel Safronov | 108 | writer exists now, so Next 1 (Feeds) can reuse it. **Done 2026-11-01 (Q5/Q6):** `blog.fuzzyworld.net` 301-redirects every path (query kept) to `writing.`; `til.fuzzyworld.net` answers 410 Gone. Both WordPress folders stay on the server, unserved; the original `.htaccess` of each is saved as `.htaccess.wp-20261101`. Backup: a WordPress export of the blog (Pavel, `~/Downloads`). The ancient `old.blog.fuzzyworld.net` copy was moved to `~/old/` on the server (it was not served). **Still to do:** building from the push hook after the move to a private repo, |
e9fdc5db 2026-11-01 Pavel Safronov | 109 | the `til` blog: **built 2026-11-01 (Q14)**, config `~/til/fw-blog.json` (4 posts, WordPress slugs kept, one image in `~/til/media/`), same generator; uploaded the same day (the WordPress folder was moved to `~/old/` on the server first, because `fw blog deploy` runs rsync `--delete`). Below, the plan as written before the |
0e603cf7 2026-10-31 Pavel Safronov | 110 | build: it still says `blog.fuzzyworld.net` in places; read that as `writing.fuzzyworld.net`. |
a4997156 2026-10-31 Pavel Safronov | 111 | |
77346d8a 2026-10-29 Pavel Safronov | 112 | **What is there** (read from its public API, 2026-10-28): |
| 113 | - Posts at `/YYYY/MM/DD/<slug>/`, an RSS feed at `/feed/`, and monthly archives. Categories hardly matter ("Uncategorized" | |
| 114 | has 138 posts). Tags are the real structure (a story series is a tag, e.g. Dice, Walkabout, Writing Excuses). | |
| 115 | - Pages: **Highlights** (the front page: a chronological list of the work I consider good, with an AI-use note), | |
| 116 | **Blog** (all posts), **Collections** (series in reading order, a hand-written list), **Upcoming**. | |
795e51b3 2026-10-29 Pavel Safronov | 117 | - 2 posts are password-protected (Rasputin, Belfast). They are not carried over (see Protected posts below). |
77346d8a 2026-10-29 Pavel Safronov | 118 | - A reading time on each post, media (images, plus epub/kpf/pdf downloads of Wick Canyon), and comments. |
| 119 | - Comments: 45 (36 top-level, 8 replies). They get imported, as static text with the author's name (no emails). | |
| 120 | ||
| 121 | **Source: `~/Blog`** (Dropbox for now; a private git repo later): 286 Markdown files, `YYYY-MM-DD-name.md`, no front | |
| 122 | matter. The title is the first `# heading`. The post is only the **`## Writing`** section; `## Spelling` (cspell words) | |
| 123 | and `## Removed writing` (cut text) are private notes and are never published. 164 files have a `## Writing` section; | |
| 124 | the others are notes. WordPress turns single line breaks into `<br>`, so the renderer must too. | |
| 125 | ||
| 126 | **Findings that shape the design:** | |
| 127 | - File names do **not** match published URLs. Only 13 of 144 posts match `date-slug`: the publish date differs from the | |
| 128 | draft date, and some names differ (`belfast-porn.md` is published as `/belfast/`). The import must record the mapping | |
795e51b3 2026-10-29 Pavel Safronov | 129 | (published date, slug, tags, highlighted) somewhere. |
77346d8a 2026-10-29 Pavel Safronov | 130 | - So the generator must keep the old URLs working: same path pattern and the same slugs. |
| 131 | ||
795e51b3 2026-10-29 Pavel Safronov | 132 | **Post metadata** (decided 2026-10-28): front matter *and* config, so the tool does not force edits to source files. |
| 133 | Precedence, highest first: | |
| 134 | ||
| 135 | 1. front matter in the file (`date`, `slug`, `tags`, `highlight`, `published`); | |
| 136 | 2. per-file overrides in the blog's JSON file, keyed by file name (a "sidecar" for people who will not or cannot edit | |
| 137 | their files); | |
| 138 | 3. defaults from the file: date and name from `YYYY-MM-DD-name.md`, title from the first `# heading`. | |
| 139 | ||
| 140 | Nothing is required: a plain folder of Markdown works. **Nothing is published unless marked** (`published` in front | |
| 141 | matter or an entry in the config): `~/Blog` has 286 files and 144 posts are live. This is the same rule as repo | |
| 142 | visibility (private unless exactly `public`). For `~/Blog`, the import writes the sidecar (your files stay untouched; | |
| 143 | easy to undo), and a later command can convert it to front matter, e.g. when the posts move to a private git repo. | |
| 144 | ||
| 145 | **Protected posts** (decided 2026-10-28): not carried over. The importer skips password-protected posts (Rasputin, | |
| 146 | Belfast) and the comments on them, and says so. They stay drafts in `~/Blog`. ๐ plugin idea for later: `.htaccess` | |
| 147 | basic auth first; client-side encryption only if needed (it adds a script and allows offline guessing). | |
| 148 | ||
77346d8a 2026-10-29 Pavel Safronov | 149 | **Design:** |
| 150 | - One JSON file describes a blog (title, base URL, source folder, which section is the post body, URL pattern, tag | |
| 151 | list, highlight list, collections, feed). The tool is `gen.py` run on a repo of Markdown, from the push hook. | |
| 152 | - Reuse what exists: Python-Markdown, Pygments, the site CSS, the feed code from step 1. | |
| 153 | - **Import (๐ plugin):** one-time script that reads the WordPress API: posts, comments, media, and a redirect map. | |
| 154 | - The editor and the Dropbox โ private-git-repo move come after the generator, not with it. | |
bb7e1f57 2026-11-06 Pavel Safronov | 155 | - **Built 2026-11-02 (Q91/Q92):** `fw blog watch <config>` (`server/blog_watch.py`; docs/blog.md, "The watcher"): a laptop |
9bb22e48 2026-11-03 Pavel Safronov | 156 | launchd agent (`fw blog watch install`) runs one tick every 60 s per blog config. After a quiet period it mirrors the |
| 157 | Dropbox source into a local git repo (snapshot commits on `dropbox-mirror`, never main) and, with `autodeploy`, deploys | |
| 158 | edits only when the set of output files, the generator (code/templates), the config and the deploy target are | |
| 159 | unchanged (anything else is held back with a notification; the baseline is `deployed.json`, also written by a manual `fw blog deploy` and guarded by the | |
| 160 | state folder's lock). Run on ONE machine only. **Not built yet:** | |
| 161 | the cutover `fw blog move <config> --to repo|dropbox` (copy, build both, require identical output, repoint the `~/Blog` | |
| 162 | symlink, keep the old copy), then Dropbox is dropped and the Air uses a clone plus a simple `fw blog sync`; the server | |
| 163 | remote and the push-hook build (Q25). | |
77346d8a 2026-10-29 Pavel Safronov | 164 | - Needs the `blog.fuzzyworld.net` subdomain pointed at a folder (DreamHost panel). |
| 165 | ||
| 166 | ## Comments | |
| 167 | ||
e9fdc5db 2026-11-01 Pavel Safronov | 168 | Decided 2026-10-28: by email, no accounts. Revised 2026-11-01: a web form (`server/comments/post.php`, written, never run |
| 169 | or installed) was designed too, but **only the | |
| 170 | email path is enabled** (Pavel: a form means code that runs on every web request, which is more attack surface than | |
| 171 | he wants; see the tiers in the README, form = T2, email = T1). The form stays in the repo, off by default. | |
| 172 | - A "comment by email" link on each post is a `mailto:` to the comments mailbox, with the post in the subject | |
| 173 | (`Comment [writing:/2024/05/01/slug/]`). A cron job (`server/comments.py ingest`) reads the comments mailbox, over IMAP | |
| 174 | (a hosted DreamHost mailbox; mail arrives through MailChannels) or from a local Maildir, and puts each comment in a pending queue. | |
| 175 | - Identity is the sender's verified address (the topmost Authentication-Results header must show DMARC pass, or DKIM | |
| 176 | pass for the From domain). The address is never shown or stored, only a hash; only the display name is shown. | |
| 177 | - Moderation: a person's first comment waits for my approval (`fw comments list|approve|reject|remove|untrust`); | |
| 178 | with `approve --trust` that sender is published on arrival from then on. Limits: 4000 characters, 5 per sender per 24 hours, 200 pending. | |
| 179 | A deletion request is honoured. | |
| 180 | - Approved comments are written as one static JSON file per post (`/_comments/<post path>.json`) which `blog.js` | |
| 181 | renders with `textContent`, so a comment appears without a rebuild. `fw blog deploy` excludes `/_comments/`. | |
| 182 | Imported WordPress comments stay baked into the pages. Readers without JavaScript see only the imported ones. | |
| 183 | - **The form (off):** `server/comments/post.php` on its own subdomain, `comments_form` in the blog config; every | |
| 184 | form comment is moderated. Enabling it is a decision for later; the upsides are no email client needed, the | |
| 185 | downsides are a public PHP endpoint, spam, and unverified identities. | |
| 186 | - **๐ Google sign-in plugin:** would need an endpoint that checks the Google token (T2), so it stays a plugin. | |
77346d8a 2026-10-29 Pavel Safronov | 187 | |
a5a72f55 2026-10-27 Pavel Safronov | 188 | ## CI dashboard |
1758548c 2026-10-27 Pavel Safronov | 189 | |
bb7e1f57 2026-11-06 Pavel Safronov | 190 | โ Built 2026-10-27 as proposed (docs/ci.md: "Watching CI"): |
1758548c 2026-10-27 Pavel Safronov | 191 | |
| 192 | - `code.fuzzyworld.net/ci/`: one static page for all public repos: queued jobs, running pipelines (which job, | |
| 193 | for how long), and the last 50 results. Rebuilt on every queue change and every upload from a runner. | |
| 194 | - Live logs: while a job runs, the runner uploads its log every ~15 seconds, so the job's page shows it growing. | |
| 195 | - Pages that show something running reload themselves every 15 seconds (`<meta http-equiv="refresh">`), so no | |
| 196 | JavaScript is needed and the site's `script-src 'none'` stays. | |
| 197 | - Private repos: never named on the public page. For those, `fw ci-list` in the terminal (and a `fw ci-watch` | |
| 198 | that follows a run and prints each job's result). | |
| 199 | ||
| 200 | ## Backlog | |
| 201 | ||
85702b14 2026-11-04 Pavel Safronov | 202 | - **URGENT (Pavel, 2026-11-04): AI usage analysis per repo, on the CI/usage pages.** There is a lot of public chatter |
| 203 | about Claude's usage limits, so each repo should show what its AI use looks like over time. It builds on the | |
| 204 | existing per-commit "Usage" tab (`repo_usage` / `usage_costs` in `server/gen.py`, from the `FW-Tokens-*` commit | |
| 205 | trailers) and on the planned "Stats and heatmap" step (Next 3). Candidate views, as static SVG charts (no JS): | |
| 206 | AI usage across time (output tokens, cached reads, cost per week or per session); AI share of commits and of pull | |
| 207 | requests over time (`Co-Authored-By`, with the Provenance caveats); lines of code added and removed over time; | |
| 208 | **usage per line of code** (output tokens, and estimated cost, per added line, per week and per commit, with the | |
| 209 | median and the hot commits already marked on the Usage tab); and a site-wide roll-up across repos. Open points: | |
| 210 | how to price tokens (a per-model price table kept in the repo, stated as an estimate); what a "line" is (added | |
| 211 | lines in non-generated, non-vendored files); sessions that span several repos; commits without trailers (the | |
| 212 | ChatGPT era) shown as unknown, never as zero. Data stays derived from git; no transcripts are published. | |
ced0eafa 2026-11-04 Pavel Safronov | 213 | **Q107 stage 1 (done 2026-11-04):** trailers that make cost computable (`FW-Tokens-Method: 2`, per-model |
| 214 | `FW-Model-Tokens`, written by `fw usage --trailers`; each message id counted once, subagents included); per-commit | |
| 215 | estimated cost from `server/prices.json` (list prices, `as_of`; unknown stays unknown); one weekly "output tokens" | |
| 216 | SVG chart on the Usage tab with a hatched strip for commits without data. Old method-1 trailers are left alone | |
| 217 | (they ran about 1.5-2x high). | |
799905ae 2026-11-04 Pavel Safronov | 218 | **Q107 stages 2 and 3 (done 2026-11-04):** the Usage tab also charts estimated cost, cached reads, average cost per |
| 219 | commit and AI-marked commits (two lines) per week or month, and the Sessions table has an est. cost column; the | |
| 220 | site-wide `/usage/` page (`build_site_usage`, called from `build_index`, public published repos only) has the same | |
| 221 | charts plus a per-repo table (unknown costs are hatched, never zero; the page is rebuilt on every `fw publish` | |
| 222 | from a per-repo summary cached in the state dir by HEAD sha). Reserved top-level names (`usage`, `ci`, `projects`, | |
| 223 | ...: `SITE_RESERVED` in `server/gen.py`) are never treated as repos by the index or the roll-up. | |
| 224 | Remaining: (4) optional: lines added/removed per week and usage per added line. | |
1758548c 2026-10-27 Pavel Safronov | 225 | - File finder and code search (one shipped script plus a prebuilt index per repo) |
| 226 | - code-server on TrueNAS, for editing in the browser (private, over Tailscale) | |
| 227 | - Provenance stats and transcript publishing (see below) | |
| 228 | - An app at every commit: `apps.fuzzyworld.net/<app>/@<sha>/` (a service worker at `/<app>/` would cover those paths; must be solved first) | |
| 229 | - OpenGraph images (link previews) from the screenshots | |
| 230 | - Deploy automatically when CI passes on `main` | |
| 231 | - Run GitHub Actions workflows through `act` (10 repos have `.github/workflows`) | |
| 232 | - Publish the shared-origin deploy checks as a reusable lint | |
c7da2b90 2026-10-29 Pavel Safronov | 233 | - Discuss: an Advent of Code wrapper app (idea added 2026-10-28; Pavel thinks it has merit). A static page: sign in |
| 234 | with GitHub, fetch the user's own puzzle data from adventofcode.com (it differs per user), show the puzzle beside | |
| 235 | an in-browser editor holding a TypeScript function that takes the logically named test data, run the code on a | |
| 236 | button press, submit the answer to adventofcode and show whether it is right. To settle: how to sign in to | |
| 237 | adventofcode from a static page (its session cookie, CORS, and its automation rules), where the code runs | |
| 238 | (a worker in the browser), and how it relates to the existing AoC harness | |
1758548c 2026-10-27 Pavel Safronov | 239 | - Found while building "Now" (2026-10-27): |
| 240 | - VS Code in the browser: Markdown preview and other webviews need a separate domain per view | |
| 241 | (a wildcard subdomain, e.g. `*.vscode.fuzzyworld.net`, set up in the DreamHost panel) | |
| 242 | - VS Code in the browser: build from current VS Code source instead of the `vscode-web` 1.91.1 package (2024) | |
| 243 | - VS Code in the browser: project-wide TypeScript IntelliSense (turned off; the web TS server rejects our URI scheme) | |
a5a72f55 2026-10-27 Pavel Safronov | 244 | - โ Renamed `.gitlab-ci.yml` โ `.fw-ci.yml` in 37 repos (2026-10-27; blister's is only local, never pushed). |
77346d8a 2026-10-29 Pavel Safronov | 245 | READMEs that said "deployed to GitLab Pages" are fixed in 11 repos (2026-10-28; edits waiting to be committed in each). |
| 246 | Still to do: the same in basin, chance-it, flight, herd, nuts, takeover (their local copies are behind, see below), | |
| 247 | the `.md` plans/designs that still say it (e.g. tower's design.md, decisions.md), and dropping `pages` jobs that only | |
| 248 | published to GitLab Pages | |
1758548c 2026-10-27 Pavel Safronov | 249 | - Old `.gitlab-ci.yml` files: `node:*-alpine` images have no `git`, so installing the |
| 250 | `fuzzyworld-tools` git dependency fails (e.g. bubbles). Switch those jobs to a `node:22` image, or delete | |
| 251 | `pages` jobs that `fw deploy` replaced | |
77346d8a 2026-10-29 Pavel Safronov | 252 | - โ The 34 merged `fuzzyworld-deploy` branches on DreamHost were deleted (2026-10-28). Local clones and the TrueNAS |
| 253 | mirror may still list `origin/fuzzyworld-deploy` until pruned (`git fetch --prune`) | |
1758548c 2026-10-27 Pavel Safronov | 254 | - CI `cache:` support (a per-repo volume on TrueNAS), and `services:` |
| 255 | - Branch pages: link to the default branch's page when a file is identical, instead of a copy | |
9eb01df7 2026-10-27 Pavel Safronov | 256 | - Found in review (2026-10-27): discussed, not done |
0a08554d 2026-10-27 Pavel Safronov | 257 | - Site pages: all now in `server/content/` (`gen.py content`), including `404.html`; `/how/` is `_how.html` (draft, not built) |
| 258 | - `/how/` goes live: finish the draft and rename `server/content/_how.html` to `how.html` (`/why/` already says it is coming) | |
9eb01df7 2026-10-27 Pavel Safronov | 259 | - CI secrets: none are set, so jobs that deploy elsewhere fail (`dreamhost`'s rsync, engye's store uploads). |
a4997156 2026-10-31 Pavel Safronov | 260 | They go in `<runner>/secrets/<repo>.env` (TrueNAS: `/mnt/main-2025/fw-runner/secrets/`; default branch and tag jobs only) |
9eb01df7 2026-10-27 Pavel Safronov | 261 | - Look at the results of the 37 rename pushes' CI runs (north-star and nuts failed; not looked at yet) |
| 262 | - explodorama: `test/demo.test.ts` "decoy" timed out at vitest's 5-second default on TrueNAS (passed the day | |
| 263 | before); a longer `testTimeout` for that test, or a faster test | |
77346d8a 2026-10-29 Pavel Safronov | 264 | - tower: its screenshot caught the loading screen. `"screenshots": [{ "path": "", "size": "phone", "wait": 8000 }]` |
| 265 | is added to `fuzzyworld` in its `package.json` (2026-10-28); it takes effect once committed and pushed, and the | |
| 266 | next CI run takes the new shot | |
9eb01df7 2026-10-27 Pavel Safronov | 267 | - Local copies behind DreamHost, with uncommitted edits in the same files as the incoming commits: |
| 268 | basin, chance-it, flight, herd; takeover has staged changes. Pull them by hand | |
| 269 | - Housekeeping (noticed 2026-10-27, never discussed): nothing is cleaned up yet | |
| 270 | - `~/fw/ci/<repo>/<sha>/` on DreamHost keeps every pipeline (pages show only the newest 100 per repo) | |
| 271 | - `runner.log` on TrueNAS grows forever | |
| 272 | - the runner's Docker images on TrueNAS (about 5 GB so far); a periodic `docker image prune` | |
1758548c 2026-10-27 Pavel Safronov | 273 | |
| 274 | ## Principles | |
| 275 | ||
| 276 | Wording approved 2026-10-26: | |
| 277 | ||
| 278 | > Things made with AI should be shared freely with the world. The code and apps here were made with | |
| 279 | > AI, so they are MIT-licensed: use them, change them, share them. | |
| 280 | > | |
ec9751fa 2026-10-27 Pavel Safronov | 281 | > My conversations with the AI are different. My words and my way of working are mine to publish or not. |
| 282 | > I will publish a conversation only when I choose to. | |
1758548c 2026-10-27 Pavel Safronov | 283 | |
| 284 | ## CI | |
| 285 | ||
bb7e1f57 2026-11-06 Pavel Safronov | 286 | โ Live since 2026-10-27. docs/ci.md is the full reference; this is the summary. It is the same |
9eb01df7 2026-10-27 Pavel Safronov | 287 | pattern as GitLab runners: **a runner registers, pulls jobs, runs them in containers, and reports back.** The |
| 288 | difference is that there's no coordinator service: the coordinator is files on DreamHost. | |
| 289 | ||
| 290 | 1. **Queue.** `post-receive` runs `fw ci-queue`, which writes `~/fw/queue/pending/<id>.json` (repo, ref, sha, | |
| 291 | default branch, time) for each pushed branch or tag with a `.fw-ci.yml`, `.gitlab-ci.yml` or `package.json`. | |
| 292 | 2. **Claim.** The runner (`runner/fw-runner.py`, TrueNAS cron every 2 minutes) runs `ssh dh claim <runner>` | |
| 293 | through `server/runner-api`. The server moves the job to `running/` with one `rename`, which is atomic, so | |
| 294 | two runners can never take the same job. The same step puts jobs running for over 2 hours back in `pending/`. | |
| 295 | 3. **Registration = an ssh key.** The runner's key is limited in `authorized_keys` to | |
| 296 | `command=".../server/runner-api"`: it can claim jobs, fetch repos and upload results, and can't get a shell. | |
| 297 | 4. **Run.** The runner clones over ssh and runs each job in a throwaway Docker container from the job's | |
| 298 | `image`. Inside jobs, `origin` is the read-only https clone URL; the runner's key never enters a container. | |
| 299 | 5. **Report.** Logs and `pipeline.json` are uploaded to `~/fw/ci/<repo>/<sha>/` (a running job's log every 15 | |
| 300 | seconds); `gen.py` renders โ/โ on the log and summary pages, a page per pipeline with its logs, a badge, | |
| 301 | and the dashboard at `/ci/`. | |
| 302 | 6. **Updates.** The runner restarts itself between jobs when `runner/install.sh` has replaced it. | |
462e50c2 2026-10-31 Pavel Safronov | 303 | 7. **Two runners.** Tags, the heartbeat lease and the laptop runner are deployed (2026-10-31; see [Runners](#runners)). |
1758548c 2026-10-27 Pavel Safronov | 304 | |
| 305 | **Job definitions: `.fw-ci.yml`** (decided 2026-10-27: our own name; the format stays a subset of GitLab CI's, | |
| 306 | so moving a repo over is a rename; the old `.gitlab-ci.yml` name still works until then). 42 repos have one, and they use only `image`, | |
| 307 | `stages`, `script`, `before_script`, `variables`, `rules` (branch conditions), `needs`, `artifacts`, `cache`. | |
| 308 | 36 of them are GitLab Pages jobs; a `pages` job maps to `fw deploy`. An unsupported keyword fails the job with | |
| 309 | a clear message; it is never silently ignored. Repos without the file fall back to `fuzzyworld.checks` from | |
| 310 | `package.json`. engye (20 KB: Android builds, Play Store, Firefox extension) is the hard case and the test of | |
| 311 | how far the subset needs to go. | |
| 312 | ||
9eb01df7 2026-10-27 Pavel Safronov | 313 | **Security.** Secrets (`/mnt/main-2025/fw-runner/secrets/<repo>.env` on TrueNAS) are given only to jobs on the |
| 314 | default branch and tags. None are set yet (see Backlog). Code from contributors will run only after I approve it, | |
| 315 | because it runs on a machine in my home (part of Pull requests). | |
1758548c 2026-10-27 Pavel Safronov | 316 | |
| 317 | ## Screenshots | |
| 318 | ||
9eb01df7 2026-10-27 Pavel Safronov | 319 | โ Live since 2026-10-27. A built-in runner job, after a successful pipeline on the default branch of an app |
| 320 | (`fuzzyworld.app` in `package.json`): | |
1758548c 2026-10-27 Pavel Safronov | 321 | |
9eb01df7 2026-10-27 Pavel Safronov | 322 | - builds the latest `main` the way `fw deploy` does (`fw deploy --here --dry-run`), serves it at `/<app>/` in a |
| 323 | Playwright container with no network, and photographs it. This is the commit CI just tested, which may be newer | |
| 324 | than what is deployed | |
1758548c 2026-10-27 Pavel Safronov | 325 | - configured in `package.json`: |
| 326 | `"screenshots": [{ "path": "", "size": "phone", "wait": 1500 }, { "path": "?demo", "size": "desktop" }]`, | |
9eb01df7 2026-10-27 Pavel Safronov | 327 | or a command for apps that need clicks first (`"screenshots": "pnpm shots"`, writing PNGs to `$FW_SHOTS_DIR`), |
| 328 | or `false`. With no config: one phone shot of the start page | |
| 329 | - apps without runner screenshots show the images in the repo's `screenshots/` folder instead (public repos) | |
| 330 | - shown on the apps index and repo summary pages; OpenGraph images are in the Backlog | |
1758548c 2026-10-27 Pavel Safronov | 331 | |
| 332 | ## Issues | |
| 333 | ||
55e5fc3e 2026-11-04 Pavel Safronov | 334 | - Reports arrive at `bugs-k7q4+<repo>@fuzzyworld.net` (only the address was live before 2026-11-03; the ingest and |
| 335 | the pages were built that day, see the task list under Report replays). | |
1758548c 2026-10-27 Pavel Safronov | 336 | - A cron job reads the mailbox (IMAP, Python standard library) and renders each thread as static pages. |
| 337 | - Reporters' email addresses are never shown. | |
| 338 | - **Configurable per repo**, the same way as visibility: `git config fuzzyworld.issues public|reviewed|private`, | |
| 339 | set with `fw issues <repo> <mode>`: | |
55e5fc3e 2026-11-04 Pavel Safronov | 340 | - `public`: a report whose sender passed email authentication is published as it arrives (an unauthenticated one waits for approval) |
1758548c 2026-10-27 Pavel Safronov | 341 | - `reviewed`: a report is published only after I mark it public |
55e5fc3e 2026-11-04 Pavel Safronov | 342 | - `private`: nothing is published; reports are still stored in `~/fw/issues` (and listed by `fw issues list`) |
1758548c 2026-10-27 Pavel Safronov | 343 | - anything other than exactly `public` or `reviewed` counts as `private`, so nothing is published by accident. |
| 344 | A private repo's issues are always private, whatever this says. | |
| 345 | - Import the GitLab issues and notes in `~/code/gitlab-export/*/issues.json`. | |
| 346 | ||
55e5fc3e 2026-11-04 Pavel Safronov | 347 | ### Report replays (Q104; planned 2026-11-03, built 2026-11-03 in this repo) |
a890b8a6 2026-11-04 Pavel Safronov | 348 | |
| 349 | Idea from the Explodorama bug mails: a report can carry a **machine-readable payload** (for the game: the run | |
| 350 | record, a few KB of JSON: seed, rules version and actions), and the issue page can then show *what the reporter | |
| 351 | saw* as a looping animation. The game side (the replay page, its limits, how old rules are loaded) is in | |
| 352 | `~/code/explodorama/PLAN.md` ยง20. This repo's part: | |
| 353 | ||
| 354 | - **Intake stays email.** DreamHost only serves files, so no upload endpoint (no abuse surface, no server code). | |
| 355 | The existing mailbox cron reads `Run record: {โฆ}` (or whatever line an app agrees on) out of a report. | |
| 356 | The mail's quoted-printable text is decoded first (the game's `tools/report-replay.mjs` already does it in | |
| 357 | Node; port the same rule to Python, and keep a test with a real report). | |
| 358 | - **Store it beside the issue.** For a repo whose issue mode allows publishing, the cron writes the payload as | |
| 359 | a static file next to the issue page (`โฆ/issues/<n>/record.json`), under the same visibility rule as the | |
| 360 | report itself: `public` as it arrives, `reviewed` only after I mark it public, `private` never. The payload | |
55e5fc3e 2026-11-04 Pavel Safronov | 361 | is validated first (size โค 64 KB, parses as finite JSON, an integer `seed` and `simVersion`, a list `actions`) and only the keys |
| 362 | `simVersion, seed, startLevel, startCharge, options, actions` are kept; a record with an address-like string is refused. | |
| 363 | Anything else stays only in my mailbox. | |
a890b8a6 2026-11-04 Pavel Safronov | 364 | Reporter emails are still never shown; the device string stays out of the page by default. |
| 365 | - **Link a viewer, don't build one.** An app announces its viewer in `package.json`, for example | |
| 366 | `"fuzzyworld": { "replay": "replay.html?report={record}" }`; with it, an issue page that has a payload shows | |
55e5fc3e 2026-11-04 Pavel Safronov | 367 | "Watch what happened", a plain link to that page of the app, with `{record}` set |
a890b8a6 2026-11-04 Pavel Safronov | 368 | to the stored file. Without the key, nothing changes. This keeps the core free of any game knowledge (๐ |
| 369 | plugin-shaped: an app opts in). | |
| 370 | - **CSP.** The viewer is the app's own script on the app's own origin, so the site-wide `script-src 'self'` | |
| 371 | policy (Q99) holds; the record is data, never inserted as HTML. The issue page itself needs no script. | |
| 372 | - **Old rules.** A viewer may need the sim of the report's own rules version. An app can publish those as | |
| 373 | static files at deploy time (Explodorama: `/explodorama/sim/<simVersion>.js`); fuzzyworld-tools only has to | |
| 374 | let `fw deploy` carry such extra files (a per-app list of globs in `package.json`) and keep old ones. The | |
| 375 | runner (TrueNAS) can build them on a tag; DreamHost has no Node. | |
| 376 | - **Admin panel (Q102).** On the laptop, the inbox lists reports with a Replay link per report (the same viewer, | |
| 377 | served locally with the dev page's `&rules=` mode, which can use any commit from git), and the buttons that | |
| 378 | mark a report public, close it, or turn it into a regression fixture in the app's repo (`test/fixtures/`). | |
| 379 | The panel is a thin local web page over `fw` commands; it holds no state of its own. | |
55e5fc3e 2026-11-04 Pavel Safronov | 380 | **Decided 2026-11-03: local-only, never hosted on DreamHost.** Every panel action is privileged (publish, |
| 381 | merge, deploy), and a login form on shared hosting would make the most valuable target on the site; the | |
| 382 | local panel's security is that it is unreachable, and its authority is the ssh key already held. It also | |
| 383 | keeps "DreamHost only serves files". Cost: no triage from a phone (reports still arrive as email, and the | |
| 384 | replay link works anywhere because it is a static page). Possible later add, not planned: one narrow | |
| 385 | endpoint behind a per-report signed link in the report mail ("mark this report public"), with no login | |
| 386 | and no general admin surface. | |
| 387 | ||
| 388 | **This repo's task list (2026-11-03; Pavel: "add to the plan, start implementing").** Items 1-4 were built | |
| 389 | on 2026-11-03 (`server/issues.py`, `lib/deploy.mjs`); the first line of each says so. Not yet committed or | |
| 390 | deployed is deliberately not recorded here. In dependency order: | |
| 391 | ||
| 392 | 1. **Issues** (built 2026-11-03; Next 5): cron ingest of the bugs mailbox over IMAP (reuse the `comments.py` / | |
| 393 | `comments-ingest.sh` plumbing), one static page per thread under `โฆ/<repo>/issues/<n>/`, | |
| 394 | `fw issues <repo> public|reviewed|private` (anything else = private), reporter addresses never shown, and | |
| 395 | a "mark public" action for `reviewed` mode. | |
| 396 | 2. **Record extraction** (built 2026-11-03): decode the quoted-printable mail, find the `Run record: {โฆ}` line, validate | |
| 397 | (โค 64 KB, valid JSON, expected keys), write `record.json` beside the issue under the issue's visibility | |
| 398 | rule; device string stays out of the page. Test with a real report (Explodorama's | |
| 399 | `test/fixtures/q101-bomb-pass.json` and its `.eml`). | |
| 400 | 3. **App opt-in** (built 2026-11-03): a `replay` key in the `"fuzzyworld"` block of `package.json` | |
| 401 | (`"replay": "replay.html?report={record}"`); the issue-page generator reads it and shows "Watch what | |
| 402 | happened". No key, no change. | |
| 403 | 4. **`fw deploy` carries extra files, kept forever** (built 2026-11-03): a per-app glob list | |
| 404 | (`"extra": ["sim/*.js"]`) published with the deploy; extra files are protected from `--delete` through an rsync | |
| 405 | filter (uploaded and updated, never removed), so a later deploy never deletes old rules versions. | |
| 406 | ||
| 407 | No CSP change (the viewer is the app's own script) and no server code on DreamHost. The admin panel (Q102, | |
| 408 | local-only) comes after all four. | |
a890b8a6 2026-11-04 Pavel Safronov | 409 | |
| 410 | **Order:** (1) the game's production replay page and limits (its PLAN ยง20.2 steps 1-2), by hand, with a stored | |
| 411 | record; (2) the cron extracting and publishing `record.json` and the `replay` key; (3) per-rules-version sim | |
| 412 | files in `fw deploy`; (4) the panel. Each step is useful alone. | |
| 413 | ||
55e5fc3e 2026-11-04 Pavel Safronov | 414 | **Decided 2026-11-03 (Pavel):** the viewer opens as its own page (a plain link, no iframe); a `reviewed` |
| 415 | report's record is published at the same moment as its text; old `sim/` files are kept forever; compact records | |
| 416 | in the game's `mailto:` link (game PLAN ยง20.2 step 4) come after the cron (step 2); the published page defaults | |
| 417 | to the plain recorded run, with comparison as an option (game PLAN ยง20.2 step 5). Nothing open here. | |
a890b8a6 2026-11-04 Pavel Safronov | 418 | |
85702b14 2026-11-04 Pavel Safronov | 419 | ## Admin panel (`fw admin`, Q102; designed 2026-11-04, decisions by Pavel) |
| 420 | ||
| 421 | **Version 1 built 2026-11-04** (`lib/admin.py`, `tests/test_admin.py`; server side: `issues list|show --json`, `issues record`, `comments list --json`). Not yet tried against the real server (tested only with a fake runner). Decisions made while building: | |
| 422 | - **Login:** a one-time login URL (random key) that sets a session cookie; everything else needs the cookie, plus the per-launch POST token and Host / Origin / Sec-Fetch-Site checks. | |
| 423 | - **Watch = dev replay:** a link to the deployed viewer when the record is public, "Download record" always, and "Prepare dev replay" (runs the app's `report-replay` npm script with the record and `--rules <commit>`; needs `npm run dev` running in the app). | |
| 424 | - **Recently approved comments** (the 20 newest) with a Remove button are in v1, not later. | |
| 425 | - **`FW_CODE_DIR`** (also `--code-dir`, default `~/code`) is configurable: where the apps' working copies live. | |
bb7e1f57 2026-11-06 Pavel Safronov | 426 | - **Deploy tab (Q111 stage 1, built 2026-11-04):** read-only; `fw deploys --json` (server/deploys.py) joins the deploy ledger (~/fw/state/deployed.json, written by `bin/deploy-tools`, keyed `<repo>/<target>`) and each app's `.fuzzyworld.json` with the repo's HEAD. Targets are declared in package.json `fuzzyworld.deploy`. Next: stage 2, a "Deploy now" button that deploys `origin/main` from a clean export (confirm page with details, background run, progress page); stage 3, any repo declares targets, apps included, tested on nordic-skating. |
85702b14 2026-11-04 Pavel Safronov | 427 | - Still not in v1: "make this a regression fixture", pull requests, CI, blog state. |
| 428 | ||
| 429 | A **local-only** web page over `fw` commands (see "Report replays": never hosted on DreamHost). `fw admin` starts a | |
| 430 | small Python (stdlib) server on `127.0.0.1` and opens the browser; every button runs the same `fw ...` command over | |
| 431 | ssh that Pavel would type, so the panel holds no state and has no authority beyond his ssh key. | |
| 432 | ||
| 433 | - **Decided:** version 1 is **reports and blog comments** together (both are "read it, then approve or reject"); | |
| 434 | **no JavaScript at first** (server-rendered pages and `POST` forms, a full reload after each action); the command | |
| 435 | is `fw admin`. | |
| 436 | - **Safety:** bind to 127.0.0.1 only; check the `Host` header (DNS rebinding); every action is a `POST` carrying a | |
| 437 | random token made at launch; a confirm step for destructive actions; never show passwords. | |
| 438 | - **Version 1:** (1) one list of reports across repos (repo, number, title, state, visibility, record, age; | |
| 439 | filters "waiting for approval" and "has a replay"); (2) a report page with the full stored text, **including | |
| 440 | unpublished reports**, the details table and thread, and Approve / Hide / Close / Reopen and the repo's mode | |
| 441 | switch; (3) blog comments: pending ones with approve, reject, trust sender; (4) a Watch link per report with a | |
| 442 | record (the dev replay page until the game's production viewer exists). | |
| 443 | - **Needs from the existing code:** `fw issues list --json`; `fw issues record <repo> <n>` (read an unpublished | |
| 444 | record); a machine-readable `fw comments list`. | |
| 445 | - **Later, each useful alone:** pull requests (after contrib intake), CI and runners, blog build/deploy and watcher | |
| 446 | state (Q100), "make this report a regression fixture" (writes `test/fixtures/` in the app's repo). | |
799905ae 2026-11-04 Pavel Safronov | 447 | - **Backlog (Q108, Pavel 2026-11-04): the admin page on the live site, working with the local `fw admin`.** A page on |
| 448 | code.fuzzyworld.net checks whether the visitor's machine runs `fw admin` (a probe of `http://127.0.0.1:<port>`) and, | |
| 449 | if so, shows the admin UI there while all data and actions go browser to localhost; the live site never sees | |
| 450 | reports, addresses or tokens (the hosted page is a static shell). Feasible, with conditions: (1) it needs JavaScript | |
| 451 | on that one page (allowed by the "local JS is OK" policy, nothing else on the site changes); (2) `fw admin` gains a | |
| 452 | JSON API with CORS for exactly the live origin, a bearer token (the current cookie login does not work | |
| 453 | cross-site), strict Origin / Host checks and the Private Network Access preflight header; (3) browsers differ: | |
| 454 | Chrome and Firefox allow an https page to call 127.0.0.1, newer Chrome asks the user once for "local network | |
| 455 | access", Safari may block it as mixed content, so test all three and keep the local page (`fw admin` as today) as | |
| 456 | the fallback; (4) the token reaches the live page through a link `fw admin` prints (token in the URL fragment, | |
| 457 | which is never sent to a server), kept in `sessionStorage`. Not started. | |
85702b14 2026-11-04 Pavel Safronov | 458 | |
1758548c 2026-10-27 Pavel Safronov | 459 | ## Pull requests |
| 460 | ||
| 461 | - Contributions arrive at `contrib-68uq+<repo>@fuzzyworld.net` as a branch URL or patches (already live). | |
55e5fc3e 2026-11-04 Pavel Safronov | 462 | **Forward-only today** (2026-11-03): like `bugs-k7q4`, it must be recreated as a fully hosted mailbox before a |
| 463 | cron job can read it over IMAP, and so that the receiving server's `Authentication-Results` header is trusted | |
| 464 | (a forwarded copy carries the destination's header instead). Give it its own account, never the comments one. | |
1758548c 2026-10-27 Pavel Safronov | 465 | - A cron job fetches each one into `refs/contrib/<n>` in the bare repo, never onto `main`. |
| 466 | - It renders a review page: the diff, CI results (after I approve the run), and the discussion from email replies. | |
| 467 | - **Transcripts.** A submitter can attach the AI conversation behind the change, so the reviewer sees exactly | |
| 468 | where they're coming from. It is the submitter's choice, published under the submitter's terms, and only on that pull request. | |
| 469 | - I merge locally, as now. | |
| 470 | - Import the GitLab merge requests in `~/code/gitlab-export/*/merge_requests.json`. | |
| 471 | ||
| 472 | ## Provenance | |
| 473 | ||
| 474 | - **AI share.** Count commits with an AI `Co-Authored-By:` trailer, per repo and site-wide. | |
| 475 | - **Transcripts** (Claude Code session โ rendered page): | |
| 476 | - opt-in, one conversation at a time | |
| 477 | - with a redaction pass (paths, email addresses, tokens) and my review before anything is published | |
| 478 | - each published transcript has a consent record: who gave permission, when, for which conversation | |
| 479 | - `fw transcript withdraw <id>` removes it from the sites | |
| 480 | - transcripts are **not** MIT; they say "shown with permission, all rights reserved" | |
| 481 | - **First candidate:** the Firefox bug-fix conversation. Permission was given 2026-10-26 and can be withdrawn. | |
| 482 | It is in a different session, so it still goes through redaction and review first. | |
| 483 | - **My own conversations** (like the ones that built this tool) are not published. |