todo: one file per item, grouped by theme, with a generated index

TODO.md had reached 1050 lines and 72 open items across 28 sections, read mostly
on a phone. It is now a directory: todo/<theme>/<item>.md, one card per file, with
frontmatter carrying id, status, created date and order, and the prose body copied
across verbatim. TODO.md becomes a generated index (bun run todo:index).

Dates: where an item's prose stated one it is used; otherwise its old section's;
otherwise 2026-08-20, floored at the day before the split rather than invented.
date_source records which.

Shaped for a two-way Kanban board, filed as todo/tooling — a card is a file, id is
the identity, frontmatter is the machine's half and the body is the owner's.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 05:42:39 +00:00
co-authored by Claude Fable 5
parent 8fa560df1b
commit 6071ded175
124 changed files with 2240 additions and 1043 deletions
+8
View File
@@ -0,0 +1,8 @@
---
theme: headscale
heading: "Headscale"
kind: theme
---
Gaps against headscale's own API, found while reading the app on 2026-08-05 — all closed the same
day. Kept here for what the policy work turned up, which is not obvious from the code.
@@ -0,0 +1,12 @@
---
id: headscale/a-node-can-be-moved-between-users
title: "A node can be moved between users"
theme: headscale
status: done
created: 2026-08-20
date_source: assumed
order: 2
---
**A node can be moved between users.** `/api/v1/node/{id}/user`, in the expanded node card next
to the tag editor — both being the things that decide which policy rules apply to a node.
+17
View File
@@ -0,0 +1,17 @@
---
id: headscale/acl-policy
title: "ACL policy"
theme: headscale
status: done
created: 2026-08-20
date_source: assumed
order: 1
---
**ACL policy.** `/api/v1/policy` GET/PUT behind `/_officer/policy`, with a plain HuJSON textarea
that sends the text byte for byte and shows headscale's verdict verbatim (line and column
included). Officer does not pre-validate: headscale owns the only parser that resolves groups,
tags and hosts, and a second weaker one would disagree with the thing that actually enforces.
**The mode cannot be read.** A file-backed policy is still served over GET; only a PUT refuses,
with "update is disabled for modes other than 'database'". So the first save is what discovers
writability, and a refusal becomes a persistent read-only banner. Verified live, not inferred.
@@ -0,0 +1,25 @@
---
id: headscale/device-invites-platform-side
title: "Device invites, platform side"
theme: headscale
status: done
created: 2026-08-05
date_source: stated
order: 5
---
**Device invites, platform side** (`COMMS/OFFSCALE_INVITE_ENROLLMENT.md` §5, built 2026-08-05).
Authorize-new-device form, the one-time link with copy/share/QR, and the invite list with revoke —
`/_officer/enroll/invites` in `invites.ts`, `InvitesView`. **The records are not Officer's.** They
proxy to the server's Officer Companion, because the joining phone has to claim without an Officer
account and this sidecar is loopback-only behind our auth; the spec's own "an invite must work when
the platform is down" argument says the same. Officer stores no invite and no claim token.
**Live on all four servers since 2026-08-05.** The 401 that blocked it was a companion-side prefix
parse (a Headscale key prefix is a fixed 12 chars and may contain `-`; they split on the first one);
fixed upstream. Verified end to end against pastilhas-eu: create returns the
`officer-offscale://join#…` link, list shows the record, revoke flips it to `revoked`. The list
envelope is not in the spec, so `pickInvites` takes the body's array whatever it is keyed under.
Four gaps were sent back to the spec author: `keys.ts` has no
sub-day key TTL for the 5-minute claim key, "the sidecar must refuse plaintext" is unenforceable
behind nginx, `tailnet` is not a headscale concept and has to be recorded on the invite, and
`/api/v1/enroll/*` collides with headscale's own namespace.
+11
View File
@@ -0,0 +1,11 @@
---
id: headscale/polling
title: "Polling"
theme: headscale
status: done
created: 2026-08-20
date_source: assumed
order: 4
---
**Polling** — `useHeadscaleNodes` has had `refetchInterval: 20_000` all along; also stale.
+12
View File
@@ -0,0 +1,12 @@
---
id: headscale/user-rename
title: "User rename"
theme: headscale
status: done
created: 2026-08-20
date_source: assumed
order: 3
---
**User rename** — was already shipped end to end (`/users/:id/rename`, `UsersView`); this entry
was stale when it was written.