gitoriaLog in with ident

tickets

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Branchmainbb64d57cmission 012 (ticket #25 API part): every /api/projects/<x>/… route takes the project's id (12-char record id) as well as its slug, same answers byte for byte; tickets stay under their project, no global /api/tickets/<x>. gate 315/0 (old code: the 13 id checks fail), connect 60/0, live-data copy old vs new 1,484 identical, slug vs id 772 identicalmremain/README.md

65.8 KB

  1. # tickets.worldapi.org
  2. The World Ticket System: the only state store of AntColony (see byrodin
  3. `/CONTAINERS/projects/antcolony/README.md`). Projects, tickets, an append-only event
  4. history per ticket (created / comment / state change), a web UI with live push, a JSON
  5. API for the scheduler/CLI, and an idempotent import of the AntColony ticket files.
  6. Written in **Hybriel** on **hl:web** (SSR + WebSocket push; hybriel master since antcolony mission 036), modelled on the hybriel
  7. repo's `projects/demo-social-network`. **Login via ident** (ticket #7, `CONCEPT.md` "Login via
  8. ident"): reading is public; writing (tickets, comments, states) needs a login; the author is
  9. the logged-in user; since ticket #20 roles per project decide who may do what (the creator-only confirm is gone); machine clients write with API tokens.
  10. See section "Login (ident)". **Markdown** in text fields and a Markdown read view for LLMs
  11. (ticket #6), **editing** by the ticket's author (ticket #8): section "Markdown, editing".
  12. **Relations** (ticket #4): parent / child and blocked-by, across projects — section "Relations".
  13. **Markdown editor** (ticket #12): every Markdown field is an `<md-editor>` — section "Markdown editor".
  14. ## Run
  15. ```bash
  16. cd /media/STORAGE/projects/tickets.worldapi.org
  17. setsid nohup ./bin/hybriel project.hl > server.log 2>&1 < /dev/null & echo $! > server.pid
  18. # stop: kill $(cat server.pid)
  19. ```
  20. * Port **8350** on 0.0.0.0 (`TICKETS_PORT` moves it): http://192.168.178.75:8350 (LAN),
  21. http://100.77.141.84:8350 (tailnet), http://127.0.0.1:8350.
  22. * Data: `storage/mpackdb/{projects,tickets,events}.*` (hl:mpackdb, primary key = mpackdb
  23. UUID `@id` everywhere — creator's convention; `TICKETS_STORAGE=<dir>` moves them).
  24. Sessions: `.sessions/` (`TICKETS_SESSIONS`). The OLD global-numbered tables
  25. `storage/tickets-*` (before ticket #38) stay as a read-only backup — nothing reads them.
  26. * The framework's dev watcher is on: saving a `.hl` file re-analyses and reloads open tabs.
  27. A restart is needed after changing `components/styles.hl` root rules, `shared/tokens.hl`, env vars or the binary.
  28. * `HL_HOST` (or `HOST`): interface to bind, default 0.0.0.0; `127.0.0.1` on Byrodin. hl:web
  29. reads it itself (hybriel#24; until antcolony mission 036 `project.hl` built the listener). `TICKETS_WATCH=0` turns
  30. the dev watcher off (the container).
  31. * Login env (section "Login (ident)"): `IDENT_API_KEY`, `IDENT_API_SECRET`,
  32. `TICKETS_CREATOR_IDENTITY`, `IDENT_URL`, `IDENT_EXCHANGE_URL`, `TICKETS_PUBLIC_URL`. Without
  33. key/secret the app runs read-only for everyone (a login answers "login is not set up").
  34. ## Projects, members, roles, new states (ticket #20 — supersedes #19 and the creator-only workflow)
  35. - **Projects are records** (`projects` table): title, slug (generated from the title — letters, digits, `.`, `_`, `-`; an admin can change
  36. it, the old slug keeps resolving), description (Markdown, the shared `<md-editor>`), members. Tickets point at their project by its **id**
  37. (never by name); `path` = `<project id>/<number>`. Pages: `/new-project`, `/<slug>` (the tickets; ticket #25 — was `/projects/<slug>`), `/projects/<slug>/settings`
  38. (admin: title, slug, description, members, invite link).
  39. - **Roles** (`members` table, one row per project + user): `use` = comment + move a ticket between open and review; `edit` = use + create, edit,
  40. assign, any state, relations; `admin` = edit + settings and members. A project always keeps at least one admin. Everybody else only reads.
  41. Any logged-in user with a display name may open a project and becomes its admin.
  42. - **Tickets** carry `createdBy` and `assignee` (members). **States**: open, progress, pending, review, reopened, done, canceled. A ticket that
  43. goes to review / pending with nobody assigned is assigned to the project's oldest admin (a second, `assign` event). The **inbox**
  44. (`/inbox`, `GET /api/inbox` with a token) shows what is assigned to you, as two lists: pending and review.
  45. - **API aliases**: the old state names are accepted as input (`in progress`, `awaiting creator`, `awaiting-creator`, `confirmed`, `rejected`, `on hold`)
  46. and mean progress / review / done / reopened / pending. Output uses only the new names.
  47. New endpoints: `GET/POST /api/projects`, `GET/POST /api/projects/:slug`, `POST /api/projects/:slug/members` (`{user, role}`; user = display name,
  48. ident id or user id), `POST …/members/remove`, `POST …/invites` (`{role, uses?, days?, email?}` → an ident invite link, ident#22),
  49. `POST /api/tickets/:ref/assign` (`{assignee}`), `GET /api/inbox`. A refused role → 403.
  50. - **Invites**: the person opens ident's link, picks an identity, ident sends them to `/login/callback?ident_code=…&invite=<id>`; only when ident
  51. says that identity accepted the invite (`/api/invites/get`) does the person join with the invite's role.
  52. - **Migration** (`lib/migrate.hl`, idempotent, runs at every start): old projects `{name}` → records, tickets re-linked by id, states renamed
  53. (awaiting creator → review, a "Question…" ticket → pending, confirmed → done, rejected → reopened, in progress → progress, on hold → pending;
  54. review / pending tickets go to the creator), `createdBy` from the created event; members: the creator (TICKETS_CREATOR_IDENTITY, read ONLY here)
  55. is admin, users named Architect / AntColonyScheduler that wrote in a project are edit, everybody else who wrote is use. It compacts the tables
  56. it changed (a second open of a table — the page realm — compacts on open and would rewrite the file under the first handle).
  57. - Short ids: `isIdentId` in `users.hl` accepts old 64-hex and new short ids (ident#23); `tools/migrate-short-ids.hl` + `tests/short-id-switch.mjs` kept.
  58. - Gate: `tests/browser.mjs` (233 checks). The pre-roles browser checks (creator-only confirm, author-only edit, relations by author, tokens page …)
  59. are PARKED in `tests/browser-pre20-parked.txt` — they need porting to the roles.
  60. ## Login (ident) — ticket #7
  61. * **How it works**: ident's identity selector top right (`<ident-selector key=IDENT_API_KEY>`
  62. from `<IDENT_URL>/selector.js`) plus a "Log in with ident" button
  63. (`<IDENT_URL>/login?key=…&return=<TICKETS_PUBLIC_URL>/login/callback`). Both hand tickets a
  64. one-time code; the SERVER exchanges it (`POST <IDENT_EXCHANGE_URL>/api/exchange` with key +
  65. secret) for the **per-app identity id**. Selector: `login.js` bridges the `ident-login` DOM
  66. event into the shell's face `identLogin` — no reload, the pages show their forms (`signedIn`
  67. push to this session's tabs). Button: `/login/callback` sets the session and redirects back to
  68. the page the login started from (ticket #10: at the click `login.js` adds
  69. `?next=<path + query>` to the button's return URL; the server follows only a same-origin
  70. path — one leading `/`, not `//`, no `\`, URL-safe chars, ≤ 500, not `/login/…` — else `/`).
  71. Logout (top right) = the shell's face `logOut`; the selector is reset (class `out` → `loggedIn = false`).
  72. * **`login.js` runs more than once per page** (ticket #9): hl:webex re-creates the header —
  73. the selector element AND the `<script>` elements — whenever the login state flips, and the
  74. browser executes a re-inserted script again. It therefore installs itself once per document
  75. (`window.__ticketsLogin`), listens for `ident-login` on `document`, always looks up the
  76. CURRENT `#selector`, and hands each one-time code to `#identcode` only once. (Before: one more
  77. listener per run → after a logout one selection = two exchanges of the same code → red banner
  78. "ident refused the login (400: unknown or already used code)", although the first logged in.)
  79. * **Users** (`storage/mpackdb/users.*`, `lib/users.hl`): one per identity id `{identity, name,
  80. created}`. The first login asks once for a **display name** (a normal field — ident may fill it
  81. later, CONCEPT point 6); writing waits for it; it cannot be changed afterwards (no UI).
  82. The session holds only `user = { id = <users @id> }` (hl:webex ships `session.user` to the
  83. page); the identity id is never in a page except the user's own `/you`.
  84. * **Authors**: new events store `user` (+ the name as `author`); the history shows the user's
  85. display name. Old events keep their free-text `author`. The web forms and the API have no
  86. author field (API: a body with `author` → 400 naming it).
  87. * **Creator**: `TICKETS_CREATOR_IDENTITY` = the creator's per-app identity id. Only that user
  88. may set `confirmed` / `rejected` (web: message; API: **403** `{error, field:"state"}`).
  89. **Unset = nobody can.** To set it on Byrodin: the creator logs in to tickets, opens **`/you`**
  90. ("Your tickets identity id", 32 hex), the architect puts `TICKETS_CREATOR_IDENTITY=<id>`
  91. into `/CONTAINERS/projects/tickets.worldapi.org/.env` and restarts the container.
  92. `/you` then says "You are the creator".
  93. * **API tokens** (`storage/mpackdb/tokens.*`): on `/you` a logged-in user creates (optional
  94. label), lists and revokes tokens. Shown ONCE (`tkt_` + 48 hex), stored as sha256 only.
  95. `Authorization: Bearer <token>` acts as that user on every API write; no / wrong / revoked
  96. token → **401** (checked before the body). Reads need no token.
  97. * **Env**: `IDENT_API_KEY` / `IDENT_API_SECRET` (tickets' registration in ident, origin
  98. `https://tickets.worldapi.org`), `TICKETS_CREATOR_IDENTITY` — all in `.env` on Byrodin (the
  99. hybriel runtime loads `.env` beside project.hl; the real environment wins; never commit it).
  100. `IDENT_URL` (default `https://ident.worldapi.org`: selector script + button),
  101. `IDENT_EXCHANGE_URL` (server side; default = IDENT_URL; `docker-compose.yml` sets
  102. `http://127.0.0.1:45002` = ident's loopback port on Byrodin), `TICKETS_PUBLIC_URL` (default
  103. `https://tickets.worldapi.org`: the button's return URL).
  104. * **ident down**: a login answers "ident did not answer" (the global `on Error` in project.hl
  105. absorbs hl:fetch's failure — it logs `error absorbed: …` for every plugin error).
  106. ## Markdown, editing (tickets #6, #8 — mission 007 (old 014))
  107. * **Markdown in text fields** (summary, comment text, state note): `lib/markdown.hl` parses the text
  108. into plain data (blocks → spans), `components/markdown.hl` builds elements from it — every
  109. character ends up in a TEXT node, no HTML string exists, so raw HTML / `<script>` shows as text.
  110. Links only when `safeHref` accepts them: `http(s):`, `mailto:`, `/path` (not `//`), `#…`;
  111. `javascript:`, `data:`, `vbscript:` etc. stay text. Understood: `#`…`######` headings (shown as
  112. h3/h4/h5), `-`/`*`/`+` and `1.`/`1)` lists (one level; a list may interrupt a paragraph), fenced
  113. code (``` / ~~~), `code`, `[text](url)`, `<https://…>`, bare http(s) URLs, `*em*`, `**strong**`,
  114. `***both***` (also `_`), `\` escapes. Single line breaks inside a paragraph are KEPT (the page
  115. showed them before). Not: quotes, tables, images, nested lists. Only pages and pushes carry the
  116. parsed blocks (`md`, `summaryMd`, `oldSummaryMd` via `lib/events.hl pageEventOf`); **API JSON unchanged**.
  117. * **Read view for LLMs**: `Accept: text/markdown` on `GET /api/tickets`, `/api/projects/:slug/tickets`,
  118. `/api/tickets/:ref`, `/api/projects/:slug/tickets/:number` → `text/markdown` (+ `Vary: Accept`),
  119. built by `lib/mdview.hl`: one ticket = `# <project> #n: <subject>`, a meta line, URL, the summary as
  120. written, `## History` with one `### <seq> · <when> · <author> <what>` per event (texts below, an
  121. edit shows "Subject before:" / "Summary before:" quoted); a list = `# Tickets (<filters>): N` +
  122. one line per ticket. Markdown wins only if it is listed with q > 0 and preferred to
  123. `application/json` (higher q, or same q and first); `*/*` / none → JSON. Errors stay JSON.
  124. `curl -s -H 'Accept: text/markdown' http://127.0.0.1:8350/api/projects/tickets.worldapi.org/tickets/6`
  125. * **Editing**: the ticket's AUTHOR (the user of its `created` event) edits subject + summary —
  126. web: "Edit" on the ticket page (form, Save/Cancel), API: `POST …/edit { subject?, summary? }`.
  127. **A ticket from before the login** (created event without a user, only an author string):
  128. **only the creator** (`TICKETS_CREATOR_IDENTITY`) may edit it. Anyone else: web message, API
  129. **403**. The edit is a new history event `kind:"edit"` with `oldSubject`/`oldSummary` +
  130. `newSubject`/`newSummary` (+ `subjectChanged`/`summaryChanged`, `isEdit`) — earlier versions stay
  131. (append-only); pushed live like a comment (the page takes over subject + summary). The page
  132. shows "X edited the ticket", "Subject before" (struck through) and the previous summary folded.
  133. No migration: only NEW events have the edit fields; existing JSON is byte-identical (mission 007 (old 014)
  134. `.scratch/m014/compat.sh`).
  135. * **SECURITY (hl:webex, patched locally until antcolony mission 036; hl:web escapes it itself, hybriel#34)**: webex put the page's state (= user text) into an
  136. inline `<script>` without escaping `</script>` → a summary/comment with
  137. `</script><img src=x onerror=…>` RAN in every viewer's browser (stored XSS, reproduced on the
  138. pre-m014 code: `node .scratch/m014/xsscheck.mjs <url>` → `__pwned = 1`). Fixed in the vendored
  139. `plugins/webex/WebFramework.hl` (seed: every `<` written as `\u003c`); to be filed as a Hybriel issue (mission 007 (old 014) report).
  140. The gate's `</script>` check (MD_RICH) still guards it.
  141. ## Markdown editor (ticket #12 — missions 009 (old 024) + antcolony 025)
  142. * The new-ticket summary, the edit summary, the comment and the state note are `<md-editor>`s
  143. around their textareas (`mdEditor { textarea { … } }`): edited visually (formatted while typing,
  144. toolbar incl. phones, shortcuts), the textarea keeps the **plain Markdown** and fires `input` as
  145. before — members, faces, API and stored data are unchanged. The state note is now a textarea
  146. (rows 1): the editor writes line breaks. Ctrl/Cmd+Enter sends the form.
  147. * A "Markdown source" button in a footer row at the bottom of every editor (antcolony mission 025, creator's
  148. follow-up request) switches it to a plain textarea with the raw Markdown, to copy it out or edit
  149. it by hand; "Visual editor" switches back (re-parses the typed text). Same `input`/`change`
  150. events and Ctrl+Enter as the visual editor.
  151. * The component is **vendored**: `shared/md-editor.js` = verbatim copy of
  152. `/media/STORAGE/projects/worldapi-components/md-editor.js` (edit it THERE, test it there, `cp` it
  153. here; `cmp` them). Served at `/md-editor.js` (project.hl), loaded once in the shell (main.hl).
  154. Its README: attributes, keyboard, lossless rules, restyling. Colours: `mdEditor { '--md-…' = token }`
  155. in `components/styles.hl`.
  156. * Without JS the page shows the plain textareas (SSR); the forms themselves need JS as before
  157. (webex faces).
  158. * Only tickets' Markdown subset exists in the editor (its parser is a port of `markdown.hl`, checked
  159. against it by the component's test with `ORACLE_APP`); pasted rich text is reduced to it; an
  160. untouched text comes back byte for byte. **Keep `markdown.hl` and md-editor.js's parser in step.**
  161. ## Relations: parent / child, blocked by (ticket #4 — mission 008 (old 017))
  162. * **Parent / child**: a ticket has at most one parent (any project). The parent's page lists its
  163. children with their states ("Children"); a child says "Part of <parent>". **Blocked by**: a ticket
  164. can wait on any number of tickets (any project); the blocked page lists "Blocked by", the other
  165. side "Blocks" — each with its state.
  166. * **Parent state** (smallest rule): `allChildrenConfirmed` = the ticket has children and every child
  167. is `confirmed`. The page shows "all children confirmed (n)" in green, else "k of n children
  168. confirmed". The parent's own state is NEVER changed by it — only the creator confirms.
  169. * **Who may set / remove a relation**: the AUTHOR of either of the two tickets (user of its created
  170. event) or the creator (`TICKETS_CREATOR_IDENTITY`). A ticket from before the login has no author:
  171. only the creator or the author of the other ticket. Replacing a parent needs the right for the old
  172. AND the new pair. Refused: a ticket as its own parent / blocker, loops (a parent below the ticket,
  173. a blocker that already waits on it, also transitively), duplicates, unknown tickets.
  174. * **Naming a ticket**: `<project>#<number>` (spaces around `#` allowed, e.g. `ident.worldapi.org#1`)
  175. or its UUID.
  176. * **Storage**: a NEW table `storage/mpackdb/links.*` (`{ kind: 'parent'|'blockedBy', ticket, other,
  177. user, created }`, indexes `@ticket`, `@other`), created empty at the first start — no migration. A
  178. removed relation is deleted there; the HISTORY keeps every change: an event `kind:"link"` on the
  179. child / the blocked ticket ("set the parent to X #1 (was Y #2)", "removed the parent X #1", "marked
  180. it blocked by X #5", "removed the blocker X #5"; extra JSON keys `isLink, linkKind ('parent' |
  181. 'blockedBy'), linkAction ('set' | 'removed'), otherRef, otherHref, previousRef` — only on link events).
  182. * **JSON**: every ticket row (list AND single GET, pushes) has `parent` (a ref or `null`), `children`,
  183. `blockedBy`, `blocks` (lists of refs, oldest link first) and `allChildrenConfirmed`. A ref =
  184. `{ id, project, number, ref ('#1'), key ('ident.worldapi.org#1'), subject, state, stateSlug, href,
  185. apiHref }`. All other keys unchanged (mission 008 (old 017) `.scratch/m017/compat.sh`).
  186. * **Markdown view**: under the URL line `Parent: <p> #n [state] subject · url`, `Children (k of n
  187. confirmed):` / `Children (all n confirmed):`, `Blocked by:`, `Blocks:` (one `- <p> #n [state]
  188. subject · url` line each); a list line ends with ` · parent … · children n, k confirmed | all
  189. confirmed · blocked by <p> #n [state], … · blocks …` (only the parts that exist).
  190. * **Web** (logged in): under "Respond" the forms "Parent ticket (project#number)" → Set parent /
  191. Remove parent, "Blocked by (project#number)" → Add blocker; a "Remove" button per blocker row.
  192. Faces `ticketSetParent(id, key)` ('' = remove), `ticketAddBlocker`, `ticketRemoveBlocker`.
  193. * **Live**: every relation write, state change and edit pushes `ticketsRelated(views)` — the relation
  194. view of the ticket and of every ticket related to it (+ one just unlinked); an open page of any of
  195. them follows without a reload (a child confirmed → the parent's "k of n" moves).
  196. * **For the scheduler**: parent tickets = rows with `children.length > 0` (don't pick them as work);
  197. a ticket waits while any `blockedBy[].state` is not what the scheduler counts as done. A report's
  198. `issues[].blocks = true` → create the issue ticket, then `POST <original>/blocked-by {"add":
  199. "<project>#<n>"}` with the scheduler's token (it is the issue's author, so it may).
  200. ## PWA — installable app, icons, offline shell (antcolony mission 046)
  201. Done the way calendar.worldapi.org does it: hl:web generates everything from `project.hl` — no JavaScript of ours.
  202. - `appTitle` "tickets", `appIcons` (192/512, `any` + `maskable`, same PNGs), `appTouchIcon`, `appFavicon`,
  203. `appThemeColor` = the header's `darker` `rgb(15, 20, 25)` (as tracker), `appBackgroundColor` = dark `rgb(25, 30, 35)` (both from `shared/tokens.hl`).
  204. Served: `/__hl/manifest.webmanifest` (linked from every head with apple-touch-icon + theme-color), `/__hl/sw.js`.
  205. - `offline = [ TicketList ]`: `/` is the ticket list, so offline it is the list as this browser last loaded it (the worker
  206. precaches `/` at install; a visited `/<slug>` is kept too). Every other page offline → hl:web's "Unavailable
  207. offline" (503). TicketList's emits are all value-form, so nothing is queued offline.
  208. - "You are offline" note in the shell (`components/main.hl` `offline`, `#offline`): hl:web has no connection state and no
  209. mount hook, so an invisible `<net-probe>` runs an endless 1 s CSS animation (`components/styles.hl` `tickets-net-tick`) and every
  210. `animationiteration` reads `navigator.onLine` (same trick as tracker).
  211. - Icons `icons/`: `icon.svg` is the SOURCE (a tilted ticket stub, purple on dark, inside the maskable safe circle r=205).
  212. Rebuild after editing it:
  213. ```bash
  214. cd icons && for s in 192 512; do rsvg-convert -w $s -h $s icon.svg -o icon-$s.png; done
  215. rsvg-convert -w 180 -h 180 icon.svg -o apple-touch-icon.png
  216. for s in 16 32 48; do rsvg-convert -w $s -h $s icon.svg -o /tmp/fav-$s.png; done
  217. magick /tmp/fav-16.png /tmp/fav-32.png /tmp/fav-48.png favicon.ico
  218. ```
  219. Each icon has its own `file` route in `project.hl` (`/favicon.ico` serves `icons/favicon.ico`).
  220. - Test: the gate's last part (browser C) — see "Test (the gate)".
  221. ## Deploy (Byrodin)
  222. Target: `/CONTAINERS/projects/tickets.worldapi.org` on Byrodin, container `tickets.worldapi.org`
  223. (`docker-compose.yml`: debian:12-slim, host network, `HL_HOST=127.0.0.1`, `TICKETS_PORT=45003`,
  224. `TICKETS_WATCH=0`, the folder mounted at `/home/tickets`, `./bin/hybriel project.hl`), public
  225. https://tickets.worldapi.org/ via nginx (TLS ends there; the vhost needs the WebSocket Upgrade
  226. headers — the live push rides `/__hl/socket`; no baseUrl/tls in the app, like notes).
  227. * **First deploy: done by the architect** (folder + data, nginx vhost, cert, DNS).
  228. * **Later: `./deploy.sh`** on Loreana, in this folder: runs the gate (refuses on a failure;
  229. `--skip-tests` skips it LOUDLY), backs up `storage/`/`.sessions/`/`.env` (what exists) to
  230. Loreana's `/media/SLOW1TB2/deploy-backups/<app>/` (newest 5 kept; an empty/failed backup stops
  231. the deploy), then rsyncs the code to
  232. `[email protected]:/CONTAINERS/projects/tickets.worldapi.org` (never `storage/`, `.sessions/`,
  233. `.env`, `.scratch/`, `server.*`, `testapp/`, logs — the preview is checked for them; no
  234. `--delete`), `docker compose up -d && docker compose restart` over `ssh -F /dev/null`, then
  235. waits for https://tickets.worldapi.org/ to answer 200. Every step is printed.
  236. * `./deploy.sh --dry-run` = gate + `rsync -n` + the commands it would run (no restart, no URL
  237. check). `--target DIR|HOST:DIR` / `--url URL` point it elsewhere (tested only against a local
  238. directory, ident STATUS "ident mission 006 (old 010)").
  239. * Import on Byrodin: `TICKETS_URL=http://127.0.0.1:45003 TICKETS_TOKEN=tkt_… ./bin/hybriel tools/import.hl` (in the container
  240. or on the host with the vendored binary).
  241. * Session cookie `hlsid` (host-only; tickets has its own host on Byrodin). Login: `.env` needs
  242. `IDENT_API_KEY`, `IDENT_API_SECRET`, later `TICKETS_CREATOR_IDENTITY` (section "Login (ident)").
  243. No migration: `users.*` / `tokens.*` are created empty at the first start; old events keep
  244. their authors (checked mission 005 (old 011): every GET of old vs new code on a copy of the store identical).
  245. ## Import the AntColony ticket files
  246. ```bash
  247. # on byrodin — refresh the copies:
  248. scp /CONTAINERS/projects/antcolony/tickets/*.md loreana:/media/STORAGE/projects/tickets.worldapi.org/import/tickets/
  249. # on loreana, with the server running (an API token of the user who should open them, /you):
  250. TICKETS_TOKEN=tkt_… ./bin/hybriel tools/import.hl # TICKETS_IMPORT_DIR (default import/tickets of the app), TICKETS_URL (default http://127.0.0.1:8350)
  251. ```
  252. Format: `project:` and `subject:` header lines, blank line, summary (`lib/import.hl`). The
  253. summary is hard-wrapped text: its lines are joined into paragraphs (one space), a blank line
  254. starts a new paragraph; a Markdown list line (`- `, `* `, `1. ` after trimming) starts a new
  255. line (`\n`) and following ordinary lines join onto it (wrapped item; nesting indentation is
  256. dropped); stored as paragraphs separated by `\n\n`, shown with `white-space: pre-wrap`. CRLF files work. Each file is POSTed to
  257. the RUNNING server (`/api/tickets` with `source` = file name); a known source answers the
  258. existing ticket, so re-running prints `0 new, N already there`. New tickets are numbered
  259. per project in sorted file-name order (output: `NEW 0004-scheduler.md → antcolony #1`).
  260. ## Ticket numbers, URLs, the migration (ticket #38)
  261. * A ticket's key is the mpackdb UUID (`id`, e.g. `0mufbphip3w7`). People use **project +
  262. number**: numbers count per project from 1 (next = highest in the project + 1).
  263. * **Slug rule: the project slug IS the project name, as is.** A new project name may only hold
  264. letters, digits, `.`, `_`, `-` (all existing names do), so it goes into URLs unescaped.
  265. * Ticket page: **`/<project>/<number>`** (e.g. `/tickets.worldapi.org/5`; ticket #25 — before it was
  266. `/projects/<project>/<number>`, which now answers 301). `/<project>` = the project's list (same as `/project/<project>`).
  267. * **Old global numbers** (`#1`…`#39` in docs and comments) are kept on the migrated tickets
  268. as `oldNumber`; the ticket page shows "formerly #38"; **`/tickets/<old n>` → 301** to the new
  269. URL (also `/tickets/<uuid>`); `/api/tickets/<old n>` keeps answering. New tickets get no old number.
  270. * Ordering never uses key order: lists by stored `updated`, a history by (`created`, `seq`),
  271. projects by `created`.
  272. * Migration 2026-09-24 (`tools/migrate-008.hl`, idempotent, header explains it): old #N →
  273. `<project> #k` by creation order. Mapping printed in `.scratch/deploy-008-<ts>/migrate-run1.txt`;
  274. or ask the API: `curl -s localhost:8350/api/tickets/38 | jq .ticket.href`.
  275. ## Short URLs (ticket #25)
  276. * **Pages**: project `/<slug>`, ticket `/<slug>/<number>` (`lib/util.hl` `projectHrefOf` / `ticketHref` — every page link
  277. and the `href` / `projectHref` of the API rows). Settings stay at `/projects/<slug>/settings`; the filter pages
  278. `/project/<slug>[/state/<s>]` and `/state/<s>` are unchanged.
  279. * **Old URLs answer 301** (`lib/api.hl`, to the project's CURRENT slug, so an old slug lands on the new one):
  280. `/projects/<slug>` → `/<slug>`, `/projects/<slug>/<n>` and `/projects/<slug>/tickets/<n>` (the form the conductor sent)
  281. → `/<slug>/<n>`, `/tickets/<old n | uuid>` → `/<slug>/<n>`. What names no project / ticket → 404 text, no Location.
  282. * **The API paths are unchanged** (`/api/projects/<slug>/…`, `/api/tickets/<ref>`); only the page links inside its answers
  283. are the short ones (`apiHref`, `settingsHref` unchanged).
  284. * **A project by its id (mission 012, ticket #25 comments 2 + 4)**: every `/api/projects/<x>/…` route takes the project's
  285. slug (also an old one) OR its id — the 12-character record id of the projects table (`[0-9a-z]{12}`, e.g.
  286. `0mufbw18nsuj` = tickets.worldapi.org; `id` in `GET /api/projects` `details` and in `GET /api/projects/<x>` `project`).
  287. It never changes, also not when the slug does. `lib/projects.hl slugOfRef` turns an id into the CURRENT slug (a slug is
  288. tried first, so it wins should a slug ever equal an id); the routes then run exactly as with the slug — the answers are
  289. byte for byte the same. Tickets stay under their project (`/api/projects/<x>/tickets/<n>`, numbers count per project);
  290. there is NO new global `/api/tickets/<x>` (the old one — old global number or ticket UUID — stays as it was).
  291. Pages are not affected (`/<slug>`, `/projects/<slug>` take slugs only).
  292. * **Route order** (hl:web: the FIRST matching route answers, authored order): explicit routes first, the two slug pages
  293. last (`lastPage = true` in `project.hl`). hl:web APPENDS its own urls (`/__hl/app.css`, `/__hl/emit`, `/__hl/sw.js`,
  294. `/__hl/manifest.webmanifest`, `/__hl/hl-runtime.js`, the component modules `/components/<file>.hl`) to the table while
  295. it is constructed, i.e. AFTER the app's — at the end of our table `/:projectSlug/:ticketNumber` still answered them
  296. with a ticket page (measured; the gate fails 15 checks and stops). So project.hl moves the `lastPage` routes to the end
  297. of `server.router.routes` right after `new WebFramework(…)`. They stay in the table itself because hl:web serves the
  298. module of a component only if a route mounts it at construction, and the service worker's page table reads it.
  299. Survives the dev watcher's re-analysis (checked). Upstream fix would be hl:web matching its own urls first.
  300. * **Reserved slugs** (`lib/projects.hl` `reservedSlugs`, lower-case compare): every first path segment a route answers
  301. (`api`, `__hl`, `components`, `login`, `projects`, `inbox`, `you`, …) plus a few for later (`my`, `assets`, `sign-in`,
  302. `settings`, `search`, …). A new project or a slug change to one → 400 `field:"slug"` "… is reserved"; a proposed slug
  303. from a title skips them (`Inbox` → `inbox-2`). The gate checks that every first segment of project.hl's routes is in the
  304. list — **a new route with a new first segment must add it there.** Live data 2026-10-03: no collision (11 slugs, no old slugs).
  305. ## Test (the gate)
  306. ```bash
  307. node tests/browser.mjs # 315 checks (mission 012), ~35 s; own server on :8352, own ident on :8353,
  308. # exchange-counting proxy on :8357 (TICKETS_GATE_EXCHANGE_PORT),
  309. # own storage in .scratch/gate-store (ident's code copied WITHOUT
  310. # .env to .scratch/gate-store/ident, mail to a sink — never real mail)
  311. ```
  312. **Ports** (mission 008 (old 017)): `TICKETS_GATE_PORT`, `TICKETS_GATE_IDENT_PORT`, `TICKETS_GATE_EXCHANGE_PORT`,
  313. `TICKETS_GATE_CHROME_A` / `TICKETS_GATE_CHROME_B` (`"8810-8814"`; default 8620-8629 / 8630-8639) — a
  314. worker with a port range runs e.g. `bash .scratch/m017/gate.sh <out.txt>` (8800-8819).
  315. `TICKETS_GATE_CHROME_C` (default 8640-8649) is the PWA browser. antcolony Mission 046 ran it as
  316. `TICKETS_GATE_PORT=8750 TICKETS_GATE_IDENT_PORT=8751 TICKETS_GATE_EXCHANGE_PORT=8752 TICKETS_GATE_CHROME_A=8753-8754
  317. TICKETS_GATE_CHROME_B=8755-8756 TICKETS_GATE_CHROME_C=8757-8759 node tests/browser.mjs` → 245 passed (~30 s).
  318. antcolony Mission 046 part (**PWA**, at the very end, own fresh Chrome C): head links manifest / apple-touch-icon / favicon /
  319. theme-color; manifest fields; every icon a real PNG of its size; favicon.ico + icon.svg; the worker registered with
  320. scope `/` and controlling; `Page.getInstallabilityErrors` empty; then the SERVER IS STOPPED and the page set offline
  321. (CDP) → reload `/` shows header + last list + `#offline`; `/inbox` → "Unavailable offline"; online again → the note goes.
  322. Screenshots `.scratch/gate-pwa-phone-online.png` / `gate-pwa-phone-offline.png` (390 px) — LOOK at them.
  323. **Ident for the gate**: the gate runs ident's CODE from `../ident.worldapi.org` (or `TICKETS_GATE_IDENT_DIR`). On 2026-10-01 ident was
  324. being re-vendored to hybriel master and its own sign-in broke (`ident /code page` timeout at browser.mjs:552; ident's own gate fails
  325. the same way). Tickets' gate then ran against a snapshot of ident's code with ident's 837fe120 bin/plugins (what live ident runs):
  326. `.scratch/w048/ident-snap` (`TICKETS_GATE_IDENT_DIR=$PWD/.scratch/w048/ident-snap`). Remake it from ident (without .env/storage/.sessions/.scratch) if ident moves on.
  327. antcolony Mission 048 "**once:**" checks: after a comment, a state change and adding a member, the new row is in the list exactly once
  328. (hybriel 64527baa+ answers a session face with a sync of session-derived members — a handler appending the row too would double it).
  329. `connected(page)` in the gate waits until the server has READ the tab's `hello` (a ping on the same socket, its pong),
  330. not just an open socket: a push sent before that reaches no component (the inbox-live check failed ~1 in 5 without it).
  331. Mission 011 part (**#25 short URLs**, after "numbers"): `/gamma`, `/gamma/1`, `/beta.example.org/2`, `/alpha/3` SSR 200;
  332. 6 old URLs → 301 to the short one (`/projects/<s>`, `/projects/<s>/<n>`, `/projects/<s>/tickets/<n>`, `/tickets/<old n>`), 6 that name
  333. nothing → 404 without Location (incl. a CR/LF number); `/nope` says no such project; hl:web's own urls keep their content types
  334. (`/__hl/app.css`, runtime, sw, manifest, client.js, `/components/{ticket,ticket_list,main}.hl`, POST `/__hl/emit`); in Chrome
  335. `/projects/gamma/tickets/1` lands on `/gamma/1`, the project link → `/gamma` and a row → `/gamma/2` without a reload; 10 reserved
  336. slugs refused at creation, a slug change to `inbox` refused; every first segment of project.hl's routes is in `reservedSlugs`;
  337. `/projects/<old slug>` → 301 `/<current slug>`. Control: without the reorder in project.hl the gate ends 35 passed, 15 failed.
  338. Mission 009 (old 024) part (**#12 Markdown editor**, before the console check): bob on his ticket — SSR has the
  339. plain textareas inside `<md-editor>`, `/md-editor.js` served, editors upgraded; a comment by REAL typing
  340. (`**x**`, `` `x` ``, `- ` list) sent with Ctrl+Enter → stored Markdown + rendered + editor cleared; a
  341. state note with the toolbar "B"; a hostile rich paste (img onerror, script, javascript: link) reduced
  342. to the subset, sent, rendered safely; the edit form opens the stored summary formatted and byte for
  343. byte, typing at the end changes only that block, saved; at 390px the new-ticket form with toolbar
  344. taps (inline code) → stored. Screenshots `gate-{phone,desktop}-mdeditor.png`, `gate-phone-newticket-mdeditor.png`.
  345. The component's own test (72 checks): `worldapi-components/test/run.mjs`.
  346. Mission 008 (old 017) part (**#4 relations**, after #8/#6): project `rel.example` — the gate user opens P (#1)
  347. and C1–C3 (#2–#4), bob X (#5). API: set parent 3 ways (per-project, UUID route + UUID, `rel.example # 1`),
  348. P's children + states, list rows carry the relations, row keys = old + exactly the 5 new, 14 refused
  349. parent writes (401, 403 ×2, itself, unknown, no `#`, loop, same, none to remove, `{}`, unknown field,
  350. non-string, author, 404) write nothing; who: bob (author of the child) sets/removes, a legacy ticket →
  351. 403 for bob, 201 for alice (creator); replace (label "(was …)"), a grandchild loop. Blocked-by: add,
  352. 13 refusals (401, 403, itself, twice, direct + 2-step loop, add+remove, `{}`, empty, unknown, not
  353. blocking, unknown field, 404), remove; non-link events keep the old keys. Markdown: child
  354. "Parent:" + "Blocked by:", parent "Children (0 of 3 confirmed):", blocker "Blocks:", list-line tails.
  355. Browsers: B (bob) on P sees 3 children + "0 of 3"; signed out: relations yes, forms no; A (alice) on C2
  356. "Part of …", "Remove parent" → B drops to 2 live, typed "Set parent" → 3 again, unknown parent →
  357. message; B on X "Blocks", A "Add blocker" → both live, bob's face refused, 3 forged faces refused, the
  358. row's "Remove"; alice confirms C1, C2 (API) → B "2 of 3", C3 (web) → "all children confirmed (3)" green,
  359. P stays open. Screenshots `gate-{phone,desktop}-{parent,blocked}.png` — look at them.
  360. Mission 007 (old 014) parts: **#8** — the gate user opens gamma #3 with a Markdown summary, edits subject
  361. (per-project URL) and summary (`/api/tickets/<uuid>/edit`), history created/edit/edit with the old
  362. values; 9 refused edits (no token 401, bob / alice (creator, not author) 403, `{}`, empty subject,
  363. same values, unknown field, non-string, author → 400) write nothing; 404s; legacy alpha #1: gate /
  364. bob 403, alice (creator) 201. In Chrome: bob's ticket (gamma #4) — "Edit" only for bob (not
  365. alice, not signed out), the form (screenshots `gate-{phone,desktop}-edit.png`,
  366. `gate-desktop-editbutton.png`), Cancel writes nothing, an empty subject is refused, Save → both
  367. browsers show the new subject + rendered summary + "bob edited the ticket" with the previous
  368. values, no reload (`gate-{phone,desktop}-markdown.png`); faces: alice refused, forged session
  369. refused; legacy alpha #1: "Edit" for alice only; logout hides it, a selector login brings it back
  370. without a reload. **#6** — JSON unchanged (non-edit events keep exactly the old keys;
  371. `Accept: application/json` = no Accept); the read view (ticket, uuid form, project list, state
  372. list, 7 Accept variants, errors stay JSON); the parser table (`tests/markdown.hl`); the rendered
  373. summary in Chrome (headings, em/strong, code, lists, code block; ONLY the 3 safe links; no
  374. script/img/handler element; raw HTML incl. `</script><img …>` shown as text; `window.__xss` unset);
  375. a Markdown comment pushed live to both, a Markdown state note. **#11** — every page wait is
  376. `TICKETS_GATE_SLOW`× (default 3) its timeout; the console check runs BEFORE ident is stopped,
  377. both browsers park on `about:blank` first, then "nothing new in the console" after.
  378. Load proof (mission 007 (old 014)): `node .scratch/m014/load.mjs <tickets url> 4 6` (4 headless Chromes ×
  379. 6 tabs looping pages, ports 8700-8719, SIGTERM closes them) while the gate runs.
  380. Ticket #7 part (login via ident, `tests/identkit.mjs`): our own ident from
  381. `../ident.worldapi.org` (or `TICKETS_GATE_IDENT_DIR`), accounts alice / bob / gate, tickets
  382. registered with origin `http://127.0.0.1:8352`, alice's per-app id computed (selector code +
  383. exchange). Server #1 WITHOUT a creator: the machine user "gate" logs in through
  384. `/login/callback` (no / unknown / reused code → 400), the name prompt, the name rules (empty,
  385. 61 chars, asked once), a token over the face, `/you` shows its id, confirm / reject → 403.
  386. Server #2 with `TICKETS_CREATOR_IDENTITY` = alice: the session survives the restart; every API
  387. write with the gate token, `author` in a body → 400, 14 unauthenticated cases → 401 (auth before
  388. the body), reads without token. Browsers: signed out = history + hint, no forms, a face write
  389. refused, screenshots `gate-{phone,desktop}-signedout.png`; A = alice signs in to ident, then the
  390. SELECTOR (no reload, `logged-in`, name prompt `gate-*-name.png`, forms appear,
  391. `gate-*-loggedin.png`); B = bob via the LOGIN BUTTON (ident /signin → /login/callback → name);
  392. writes show alice / bob / gate as authors; bob's confirm / reject refused (web + API 403), alice
  393. confirms; bob's token on /you (shown once, `gate-*-tokens.png`, not after reload) → API write as
  394. bob → revoke → 401; alice cannot revoke bob's token; 8 faces with a forged session (#31) refused;
  395. old events keep "creator" / "worker"; logout resets the selector without a reload; ident stopped
  396. → "ident did not answer" (400, no 500). Ident's log: `.scratch/gate-ident.log`.
  397. Ticket #9: tickets reaches ident's `/api/exchange` through a proxy in the gate that COUNTS the
  398. exchanges. alice gets a 2nd identity "alice two"; on a ticket page: logged in → Log out →
  399. selector → "alice two": no `#loginerror`, exactly ONE exchange (200), no reload, `/you` shows
  400. alice two's per-app id; then Log out → in-app click to the list → selector → "Default": again
  401. one exchange, logged in as alice. Ticket #10: `/login/callback?next=` table (16 cases: paths
  402. kept, `//host`, `https://`, `/\host`, `javascript:`, CR/LF, quotes, `/login/…`, 501 chars → `/`);
  403. in Chrome bob logs out, clicks to a ticket (pushState URL), "Log in with ident" → ident → back
  404. on that ticket, logged in; same from `/state/open`.
  405. Manual repro of #9 with instrumentation (listeners, `change`s, socket frames, exchanges):
  406. `node .scratch/m012/repro.mjs [appDir]` (`SCEN=ticket-nav|list|ticket-direct`, `SECOND=<identity>`;
  407. ident :8363, proxy :8366, tickets :8362, Chrome 8640-8649; output `.scratch/m012/repro-run/out.txt`).
  408. Ticket #38 part (runs first): `tests/oldstore.hl` writes an OLD-format store (5 tickets,
  409. old #1–#5 in `alpha` / `beta.example.org`, a same-ms tie), `tools/migrate-008.hl` migrates a
  410. copy of it TWICE (second run must print `0 new`), the server runs on the migrated tables:
  411. old-number and per-project API (GET + POSTs), `/tickets/<n>` 301 (fetch and in Chrome),
  412. "formerly #n", a real click on a list row, new tickets continue the numbering (alpha #4,
  413. new project gamma #1), pushed rows carry `/projects/…` hrefs (list + inbox in browser B).
  414. Screenshots also `gate-{phone,desktop}-legacy.png`.
  415. It starts its own server, runs the import twice, drives the API, and opens TWO headless
  416. Chromes (debug ports 8620-8629 / 8630-8639, `--disable-gpu`, killed by PID at the end):
  417. filters by real clicks, a comment and state changes in browser A appear live in browser B
  418. (no reload — checked with a window marker), API writes appear live in both, the inbox and
  419. header count follow, the new-ticket form, and the layout at 390px and 1280px (no horizontal
  420. overflow; screenshots `.scratch/gate-{phone,desktop}-{list,ticket,inbox}.png`,
  421. `gate-phone-newticket.png`, `gate-phone-paragraphs.png` — look at them). Also: the strict-API
  422. table (26 refused bodies, nothing written), Vienna time against node's `Intl`
  423. (`bin/hybriel tests/localtime.hl` prints DST-edge samples), and a CRLF paragraph fixture
  424. imported from `.scratch/gate-import` (→ #7). Ticket #15 (author cases replaced by ticket #7's "no author field"), 13 invalid
  425. JSON bodies → 400 at the right character and no source path, valid escapes still accepted,
  426. no name field in either form, a list fixture `.scratch/gate-import-lists` (→ #8) rendered as
  427. separate lines (`gate-phone-lists.png`). Server log: `.scratch/gate-server.log`.
  428. Chrome: `HL_CHROME` (default `/opt/google/chrome/chrome`, google-chrome is not on PATH).
  429. Check for leaked Chromes afterwards: `ps -eo pid,args | grep [h]l-browser-tier` (must be empty).
  430. ### Short URLs on real data (ticket #25)
  431. ```bash
  432. # a copy of the live tables (never run on the original), then a server on it (port of your range, watcher off)
  433. tar -C /CONTAINERS/projects/tickets.worldapi.org -cf - storage/mpackdb | ssh loreana tar -C /media/STORAGE/projects/tickets.worldapi.org/.scratch/w083/realdata -xf -
  434. mkdir -p .scratch/w083/run && cp -a .scratch/w083/realdata/storage .scratch/w083/run/ # a fresh copy per run
  435. TICKETS_PORT=8763 TICKETS_STORAGE=$PWD/.scratch/w083/run/storage/mpackdb TICKETS_SESSIONS=$PWD/.scratch/w083/run/sess TICKETS_WATCH=0 ./bin/hybriel project.hl &
  436. node tests/shorturls-realdata.mjs http://127.0.0.1:8763 20 # every project + 20 tickets: short URL 200 (the page), every old URL 301 → it → 200; collisions with reserved slugs
  437. ```
  438. Mission 011: 187 passed, 0 failed (11 projects, 20 tickets). The API of the old and new code on the same copy
  439. (`.scratch/w083/apicompare.py <old url> <new url>`): 1,348 answers (JSON + Markdown), 17 identical, 1,331 differ only by
  440. the page links (`href` values, the Markdown URL lines), 0 different.
  441. ### Project ids on real data (mission 012)
  442. ```bash
  443. # a copy of the live tables (as above, into .scratch/<you>/realdata), the OLD tree (as in "Same output" step 2), each on a fresh copy:
  444. TICKETS_PORT=8763 TICKETS_STORAGE=<old copy>/mpackdb TICKETS_SESSIONS=<dir> TICKETS_WATCH=0 ./bin/hybriel project.hl & # in the old tree
  445. TICKETS_PORT=8764 TICKETS_STORAGE=<new copy>/mpackdb TICKETS_SESSIONS=<dir> TICKETS_WATCH=0 ./bin/hybriel project.hl & # here
  446. python3 tests/projectids-realdata.py http://127.0.0.1:8763 http://127.0.0.1:8764 # must end "0 FAILED"; then kill both by PID
  447. ```
  448. Old vs new: every API read (JSON + Markdown) byte for byte; on new: every `/api/projects/<slug>/…` read again with the
  449. id = the same bytes; unknown id and `/api/tickets/<project id>` → 404. Mission 012 (Byrodin copy 2026-10-04 ~05:00):
  450. 11 projects, 309 tickets, 1,484 old-vs-new answers + 772 slug-vs-id answers, 0 FAILED. The gate's part: "ids (#25)" (22
  451. checks: reads by id = slug form byte for byte, every write route by id, unknown id 404, id after a slug change); on the
  452. old code exactly those fail (control run: 286 passed, 13 failed, then it stops).
  453. ### Same output (a cleanup must not change behaviour — mission 010)
  454. The gate proves the features; this proves a refactor answers byte for byte what the old code answered, on REAL data.
  455. ```bash
  456. # 1. a copy of the live tables (Byrodin → Loreana; opening a table rewrites its index files: never run on the original)
  457. tar -C /CONTAINERS/projects/tickets.worldapi.org -cf - storage/mpackdb | ssh loreana tar -C /media/STORAGE/projects/tickets.worldapi.org/.scratch/realdata -xf -
  458. # 2. the old code as a tree of its own (bin + plugins included), e.g. before the change:
  459. rsync -a --exclude /.scratch --exclude /storage --exclude /.sessions --exclude '/.env*' --exclude /.git --exclude '/server.*' --exclude /import ./ .scratch/old/
  460. # 3. both trees on a fresh copy each: ~2,200 answers — every page signed in (as az5b2, users @id 0mufikk6hj1u, through a
  461. # session file) AND signed out, every API read as JSON and Markdown, refusals, then ~70 writes (API + faces) and the reads after
  462. node tests/realdata-baseline.mjs .scratch/old 8750 .scratch/base-old
  463. node tests/realdata-baseline.mjs . 8750 .scratch/base-new
  464. python3 tests/realdata-compare.py .scratch/base-old .scratch/base-new # reads: must say 0 DIFFERENT
  465. python3 tests/realdata-compare-writes.py .scratch/base-old .scratch/base-new # writes: must say 0 DIFFERENT
  466. python3 tests/letcount.py . # code order: 1 root .hl, never-reassigned let 0
  467. ```
  468. Masked (not app output): the session seed's `at` time, the View's source positions (`site`, `Child@line:col` mount keys)
  469. and the `?v=` content hashes; in writes also the new ids, today's times and the token. Two runs of the SAME code compare
  470. clean with these masks (checked). Mission 010 numbers: 2,172 reads (1,460 byte-identical, 712 identical after masking),
  471. 72 write answers identical after masking.
  472. ## Pages
  473. | Route | Component | |
  474. |---|---|---|
  475. | `/` | `components/ticket_list.hl` | all tickets, newest update first; project + state filter chips; "New ticket" form |
  476. | `/project/:projectName`, `/state/:stateSlug`, `/project/:p/state/:s` | same | filtered (state slug: `in-progress`, `awaiting-creator`, `on-hold`, …) |
  477. | `/:projectSlug/:ticketNumber` (last, ticket #25) | `components/ticket.hl` | ticket, history, comment form, state change (select + optional note), "Edit" for the author (#8); texts rendered as Markdown (#6) |
  478. | `/:projectName` (last, ticket #25) | `components/ticket_list.hl` | the project's page (= `/project/:projectName`) |
  479. | `/projects/:slug`, `/projects/:slug/:number`, `/projects/:slug/tickets/:number` | function routes | 301 → `/<slug>` / `/<slug>/<n>` (current slug), else 404 (ticket #25) |
  480. | `/tickets/:ref` | function route | 301 → `/<p>/<n>` (`:ref` = old global number or UUID), else 404 |
  481. | `/inbox` | `components/inbox.hl` | every ticket in `awaiting creator`, across projects (the creator's comment on one sets it to `answered` and it leaves the inbox) |
  482. | `/you` | `components/you.hl` | the logged-in user: display name, own per-app identity id, creator or not; API tokens (create / list / revoke) |
  483. | `/login/callback` | function route | the login button's return: `?ident_code=` → exchange → session → 302 to `?next=` (same-origin path only, else `/`; 400 page on failure) |
  484. | `/login.js` | file | the selector bridge (`ident-login` → the shell's hidden `#identcode`, once per code) + `next=` on the login button |
  485. Shell: `components/main.hl` (header, Inbox link with live count, top right the login: selector +
  486. button, or name link to `/you` + Log out; the display-name prompt). All CSS: `components/styles.hl`
  487. (tokens imported from `shared/tokens.hl` — section "Design tokens"; accent `colorAccent = var(purple)`
  488. = #c586c0, `--color-danger: var(--red)` = #f44747; domain tags `<ticket-board>`, `<ticket-state>`, `<ticket-view>`,
  489. `<event-head>`, …; mobile first, one `min-width: 45rem` block).
  490. Live push (audience in `project.hl`): `ticketCreated(row, inboxCount)` and
  491. `ticketEvent(ticketId (UUID), event, row, inboxCount)` (public), raised by the web faces AND the
  492. API; `signedIn(tag, {name, named, creator})` / `signedOut(tag)` only to the tabs of the session
  493. whose login carries that random tag (`session.data.tag`).
  494. ## API (JSON)
  495. `:ref` = the OLD global number of a migrated ticket (`38`) or the ticket's UUID (`id`).
  496. `:slug` = the project's slug (also an old one) or its id (12 characters, mission 012 — every `/api/projects/:slug/…` route),
  497. `:number` = per-project number. Both forms answer the same shapes.
  498. | Method + path | Body | Answer |
  499. |---|---|---|
  500. | `GET /api/tickets[?project=&state=]` | — | `{ tickets: [row] }` (state as `awaiting creator` or slug) |
  501. | `POST /api/tickets` | `{ project, subject, summary?, source? }` | 201 `{ ticket, existed:false }`; known `source` → 200 `{ ticket, existed:true }` |
  502. | `GET /api/tickets/:ref` | — | `{ ticket, events: [event] }` (oldest first); 404 |
  503. | `POST /api/tickets/:ref/comments` | `{ text }` | 201 `{ ticket, event }` |
  504. | `POST /api/tickets/:ref/state` | `{ state, text? }` | 201 `{ ticket, event }`; 400 unknown/same state; 403 confirmed/rejected by a non-creator |
  505. | `GET /api/projects/:slug/tickets[?state=]` | — | `{ tickets }`; 404 unknown project |
  506. | `POST /api/projects/:slug/tickets` | `{ subject, summary?, source? }` | as `POST /api/tickets` |
  507. | `GET /api/projects/:slug/tickets/:number` | — | `{ ticket, events }`; 404 |
  508. | `POST /api/projects/:slug/tickets/:number/comments` | `{ text }` | 201 `{ ticket, event }` |
  509. | `POST /api/projects/:slug/tickets/:number/state` | `{ state, text? }` | 201 `{ ticket, event }`; 403 as above |
  510. | `POST /api/tickets/:ref/edit`, `POST /api/projects/:slug/tickets/:number/edit` | `{ subject?, summary? }` (≥ 1) | 201 `{ ticket, event (kind edit) }`; 403 not the author; 400 empty subject / nothing changed |
  511. | `POST …/parent` (both forms) | `{ parent }` — `<project>#<n>` or UUID; `""` removes it | 201 `{ ticket, event (kind link) }`; 403 neither author nor creator; 400 `field:"parent"` (unknown, itself, loop, same, none to remove) |
  512. | `POST …/blocked-by` (both forms) | `{ add }` or `{ remove }` (exactly one) | 201 `{ ticket, event (kind link) }`; 403 as above; 400 `field:"add"`/`"remove"` (unknown, itself, loop, twice, not blocking) |
  513. Every ticket GET answers `Accept: text/markdown` with a Markdown document (section "Markdown, editing").
  514. **Every POST needs `Authorization: Bearer <token>`** (ticket #7; a token from `/you`): missing,
  515. malformed, unknown or revoked → **401** `{ error }` + `WWW-Authenticate: Bearer`, checked
  516. before the body. The author is the token's user. GETs need nothing.
  517. | `GET /api/projects` | — | `{ projects, states }` (projects in creation order) |
  518. Ticket row: `id` (UUID), `project`, `number`, `ref` (`#5`), `href` (`/<p>/<n>`, ticket #25),
  519. `apiHref`, `oldNumber` (only if migrated) + `oldRef`/`hasOld`, `subject`, `summary`,
  520. `state`, `stateSlug`, `source`, `created`/`updated` (+`Ms`), `events`, `projectHref`.
  521. Event row: `id` (UUID), `ticket` (UUID), `project`, `number`, `seq`, `kind`, `author`,
  522. `text`, `from`, `to`, `when`, `createdMs`, …
  523. **Since #38 `ticket.id` is a UUID, not a number** — scripts that did `/api/tickets/${ticket.id}`
  524. keep working (UUIDs are accepted); for display use `project` + `ref`.
  525. **Strict** (ticket #10, `bodyError` in `lib/api-helpers.hl`): a POST body must be a JSON object; an unknown
  526. field, a missing or empty (after trim) required field, or a non-string value (null included)
  527. → **400 `{ error, field }`** naming the field, and nothing is written. Semantic refusals
  528. (unknown/same state, bad project name) also carry `field`. `author` is gone (ticket #7): a body
  529. that has it → 400 `field:"author"` ("no field 'author' any more: the author is the user of your
  530. API token"). Invalid JSON → **400 `{ error: "invalid JSON at
  531. character N" }`** (no `field`, no source path): `lib/jsoncheck.hl` checks the syntax BEFORE
  532. `JSON.parse` — a WORKAROUND for hybriel #12 (no soft parse); remove it (and `readBody`'s call
  533. in `lib/api-helpers.hl`) once #12 is fixed. A LONE `\uD800`–`\uDFFF` escape is refused
  534. too, because hl's `JSON.parse` aborts on it; a valid pair (`\ud83d\ude00`, Python's default
  535. `ensure_ascii=True`) is accepted since hybriel#15 (antcolony mission 036). Other errors: `{ error }` with 404/405.
  536. **Times**: `created` / `updated` / `when` are Europe/Vienna wall time `YYYY-MM-DD HH:MM`
  537. (`lib/util.hl` `localStamp`: EU DST rule, correct from 1996 on — hl:time has no time zones);
  538. `updatedMs` / `createdMs` and the stored values are epoch ms (UTC). Example:
  539. ```bash
  540. curl -s -XPOST -H "Authorization: Bearer $TICKETS_TOKEN" -d '{"state":"awaiting creator","text":"done"}' http://127.0.0.1:8350/api/projects/tickets.worldapi.org/tickets/5/state
  541. curl -s http://127.0.0.1:8350/api/tickets/38 | jq '.ticket | {project, number, href, oldNumber}' # old number still works
  542. ```
  543. Query the store directly: `curl -s http://127.0.0.1:8350/api/tickets | jq` (the mpackdb files
  544. are binary; the API is the query tool).
  545. ## Connecting an app to a project (ticket #21)
  546. Another app (first: gitoria) is connected to ONE project; it then gets a key that reaches that project only. Code: `lib/connections.hl`,
  547. `components/connect.hl`, the connections line on the project page (`components/ticket_list.hl`), routes in `project.hl`. Data:
  548. `storage/mpackdb/connections.db` (created empty at the first start; no migration).
  549. 1. The app sends the browser to `GET /connect?app=<name>&label=<owner/repo>&return=<url>&state=<opaque>` (`label` and `state` optional; the state
  550. comes back unchanged). The return URL must be https on `worldapi.org` or a subdomain — or start with an origin listed in the env
  551. `TICKETS_CONNECT_ORIGINS` (comma separated, for dev copies). Else 400. A valid request is stored (1 hour) and the browser goes to `/connect/<nonce>`.
  552. 2. On that page the person, logged in through ident with a display name, picks a project they are ADMIN of or makes a new one (they become its
  553. admin) and confirms. The page says which app and host ask, then links back: `<return>?code=<one-time code>&state=<state>` (code valid 5 minutes).
  554. A second connection of the same app + label to the same project replaces the first (its key dies).
  555. 3. The app's SERVER swaps the code: `POST /api/connect/exchange {"code": "…"}` → `200 {key, project (slug), title, api}`. The key is `tktc_` + 48 hex,
  556. shown once, only its sha256 is stored. Wrong / used / expired code → 400.
  557. 4. The key: `Authorization: Bearer tktc_…` plus, on every write, `X-Tickets-Identity: <ident public id of the person>`. It works only on
  558. `POST /api/projects/<slug>/tickets` and `POST /api/tickets` (create), `…/comments` and `…/state` of tickets of ITS project — the same
  559. endpoints a user token uses, so "mentioned in PR" / "fixed by commit" is a comment or a state change with a note. The named person must have
  560. logged in to tickets once and chosen a display name; **the project's roles apply to them** (use: comment, open ↔ review; edit / admin: more).
  561. The author of what is written is that person. A key on any other endpoint (settings, members, invites, edit, assign, relations, inbox,
  562. new project, disconnect) → 401; another project → 403; no / unknown identity → 403 naming the header. Reads need nothing.
  563. 5. The project page shows "Connected to <app>: <label>" to everybody; an admin's **Disconnect** deletes the connection — the key is dead at once.
  564. `GET /api/projects/<slug>/connections` lists them; `POST /api/projects/<slug>/connections/remove {"id"}` (user token, admin) disconnects.
  565. Short copy for app workers: `docs/connect-apps.md` (the architect copies it next to antcolony-docs).
  566. Gate: `node tests/connect.mjs` (ports 8700 / 8701 + Chrome 8703–8709; an ident stub, a real headless Chrome) → 60 checks.
  567. ## Design tokens (shared, ticket antcolony#3 — antcolony mission 021)
  568. * **`shared/tokens.hl`** declares the WorldAPI palette + semantic tokens as hl:web css variables,
  569. hand-written: `static dark = var('rgb(25, 30, 35)')`, `static colorText = var(light)`, …
  570. (`import { var } from 'hl:web/css'`). It is a copy of the one source
  571. `loreana:/media/STORAGE/projects/worldapi-tokens/tokens.hl` (vendored like `plugins/`, no generator):
  572. edit it THERE, then `cp /media/STORAGE/projects/worldapi-tokens/tokens.hl shared/tokens.hl` in every
  573. app and restart it. Check: `cmp /media/STORAGE/projects/worldapi-tokens/tokens.hl shared/tokens.hl`.
  574. (2026-09-26: the apps' copy = gitoria's/ident's; it differs from the source only in 3 COMMENT lines that
  575. still say webex — update the source's comments, then the check is byte-exact again.)
  576. * `components/styles.hl` IMPORTS the tokens it uses (`import { colorText, colorBorder, … } from '../shared/tokens.hl'`)
  577. and writes them as members: `color = colorText`, `border = '1px solid ' + colorBorder`. It sets only
  578. its accent: `colorAccent = var(purple)` (#c586c0). A token it uses must be in the import list,
  579. a new token must be added to tokens.hl (static) first.
  580. * hl:web names each token after its member (`colorTextMuted` → `--color-text-muted`), writes EVERY
  581. token of tokens.hl into the one `:root` (declaration order), then the app's `colorAccent` (a second
  582. `--color-accent`, later wins), and writes each use as `var(--…)`. Components/JS may still use
  583. `var(--color-…)` strings — the custom property names are the same.
  584. * hl:web does this itself since hybriel#39 (the webex LOCAL PATCH of antcolony mission 021 is gone, antcolony mission 036).
  585. * A change of tokens.hl or of components/styles.hl root members needs a RESTART: the dev watcher re-analyses
  586. but the served sheet keeps the old values (measured, antcolony mission 021).
  587. * Deploy: `shared/` is part of the app folder; `deploy.sh`'s rsync sends it (proved in antcolony mission 021:
  588. deploy.sh excludes + debian:12-slim container → byte-identical `/__hl/app.css`).
  589. ## Vendored Hybriel
  590. **hybriel master 06617221** (antcolony mission 074, 2026-10-03; adds the plugin allocators 3a781359 + 413f60e4 (#126:
  591. every plugin allocates with malloc via plugin_api.zig), mpackdb 2cb7ae5e (frees per-operation buffers), http1 773de63e (request
  592. owns its parse), f0ac2d2d (event order; plugin ABI field — bin and .so must match); no lambda semantics change). sha256
  593. `21059cc741459ded8a004d44ed17c071aa296609be9fd0359202383210d77bdb`, built the same way from `git archive 06617221`
  594. (`~/scratch-074/src`, removed). Old vendor (190aa11d) in `.scratch/pre-074/`. Gates on 8760–8769: `TICKETS_GATE_PORT=8760
  595. TICKETS_GATE_IDENT_PORT=8761 TICKETS_GATE_EXCHANGE_PORT=8762 TICKETS_GATE_CHROME_A=8763-8764 TICKETS_GATE_CHROME_B=8767-8768
  596. TICKETS_GATE_CHROME_C=8769-8769 node tests/browser.mjs` → 249/0; connect.mjs as a temp copy (app URL :8762, Chrome 8763–8769,
  597. TICKETS_CONNECT_PORT=8760 TICKETS_CONNECT_IDENT_PORT=8761) → 60/0. Memory: `LONG=1 node .scratch/w074/mem.mjs <tag> <app tree>
  598. 8760` (live-storage copy in `.scratch/w074/realdata/storage`, deleted after the run; prints RSS + swap): new 315 → 335 (200) →
  599. 312 (1200) → 315 MiB (5700 pairs) — **flat**; old 190aa11d 319 → 358 → 451 → 743 MiB.
  600. Before: **hybriel master 190aa11d** (antcolony mission 072, 2026-10-02; adds fc838894 GC correctness (string index / plugin error message
  601. read freed memory), 038d84b3 (#126 returned closure scopes collected), #127 (d98926c6, 04df4428); no lambda semantics change since
  602. 7eea0d32). `bin/hybriel` sha256 `860f5e61878be75fe84a569eee03c95a25f2757940903b2ed0afee0871a23626`, built read-only from
  603. `git archive 190aa11d` (`~/scratch-072/src`, removed) with `/media/STORAGE/projects/hybriel/native/zig-toolchain/zig build
  604. -Doptimize=ReleaseFast -Dtarget=x86_64-linux-gnu.2.39` in `native/`; old copy (7eea0d32) in `.scratch/pre-072/`.
  605. Memory proof: `node .scratch/w072/mem.mjs <tag> <app tree> <port>` (needs a live-storage copy in `.scratch/w072/realdata/storage`,
  606. deleted after each run — re-tar it; env LONG=1 adds 3×1500): new 315 → 355 (200) → 418 (1200) → 706 MiB (5700), old 697 → 1365 → 1719 MiB.
  607. Before: **hybriel master 7eea0d32** (antcolony mission 069, 2026-10-02; ≥ f685f240 = #126 collector also by bytes, includes #48 #112 #117 #119 #120).
  608. `bin/hybriel` sha256 `f0d3019f46f83cbf119d0a426fab18394bfc6c02c58abaa725983fcc4bc734a0`, built read-only
  609. (`git -C /media/STORAGE/projects/hybriel archive master | tar -x -C ~/scratch-…/src`, then
  610. `cd native && /media/STORAGE/projects/termuplex/.tools/zig/zig build -Dtarget=x86_64-linux-gnu.2.39 -Doptimize=ReleaseFast`).
  611. `plugins/{core,crypto,data,fetch,fs,http,http1,mpackdb,proc,time,web}` = master's — **no local patch**.
  612. Re-vendor = copy the binary + these plugins, run the gate. The previous copy (73267707, sha 9707e0cc…) is in `.scratch/pre-069/`
  613. (the one before, 837fe120, in `.scratch/pre-048/`).
  614. **Lambda parameters COPY their argument since hybriel #48** (like methods); `&p` makes it a reference. A lambda that fills a list/record
  615. it was given needs `&`: `migrate.hl` `migrateProjects/Tickets/Events/Members = (&log, …)` (without it the gate fails at
  616. "migration: at the first start the log says what was migrated"). Audit script `.scratch/w069/audit.py <files>` (param mutations +
  617. `for (x of param)` aliases). `store.hl` `sortByUpdated(rows)` sorts its copy and RETURNS it — callers use the return, fine.
  618. antcolony Mission 048: `migrate.hl`'s `compactNow()` block (hybriel #110) removed (proof logs `.scratch/w048/`). Lesson (hybriel #122):
  619. a post-face session sync must keep the route params — repro `.scratch/w048/repro/` (`node repro.mjs <bin>` must keep "you may edit").
  620. The old webex generation (e565176b + LOCAL
  621. PATCHes #34 seed escape, #39 tokens) is backed up in `.scratch/pre-036/` (bin, plugins, sources, tests).
  622. * Framework pages use content-hashed URLs `/__hl/{hl-runtime.js,web/client.js,app.css}?v=…` (hashed =
  623. `immutable`, bare = `no-cache`). Check: `~/scratch-036/hashcheck.sh tickets.worldapi.org TICKETS /` (own server :8730).
  624. * Dropped with antcolony mission 036: plugins/webex + both LOCAL PATCHes, the NativeWebSocketServer listener
  625. (HL_HOST, #24), `sessions.resolve(cookie)` in /login/callback (→ `req.session`, #11), `realSession()`
  626. (→ `session == null`; hl:web refuses a forged trailing session itself, #16 — the gate's 12 forged checks
  627. accept that ack `ok:false` "the `session` parameter is filled by the server"), the hand URL encoder
  628. (→ `encodeURIComponent`, #14), import.hl's charCodeAt string order (→ `a > b`, #2), jsoncheck's refusal
  629. of VALID surrogate pairs (#15; lone surrogates still abort JSON.parse → still 400).
  630. * Kept on purpose: `jsoncheck.hl` (JSON.parse still aborts on bad input), `localtime.hl` (no time zones),
  631. hand insertion sorts (no list `sort()`), `login.js` once-per-document guard (harmless).
  632. The binary finds `plugins/` beside the app (it walks up from the script's directory).
  633. `tests/cdp.mjs` + `tests/ports.mjs` are copies of the hybriel repo's `tests/browser/`.
  634. ## Files
  635. Code order (antcolony `docs/code-order.md`, mission 010): `project.hl` is the map, no other `.hl` in the root, one `lib/`
  636. file per topic holding its central logic and every write to its tables; the API routes and the faces are thin wrappers.
  637. Imports only go downwards (Hybriel refuses an import cycle): util ← users ← projects ← tickets-helpers ← events ← tickets
  638. ← invites / connections ← api-helpers ← api ← project.hl.
  639. | File | |
  640. |---|---|
  641. | `project.hl` | THE MAP: index comment (feature → file), config (PWA, port, env), routes, audience (who hears which push), migrate at start, the server |
  642. | `lib/tickets.hl` | tickets + their relations: `tickets` and `links` tables, rows, finding (`ticketAt`, `ticketByRef`, `ticketByKey`), lists, inbox, create / comment / state / assign / edit, parent / blocked-by, `removeMember` (also unassigns), `ticketRights` |
  643. | `lib/tickets-helpers.hl` | state names + old aliases (`stateOf`), a related ticket's ref, the relation view, the list's filter links |
  644. | `lib/events.hl` | the history: `events` table (append only, `putEvent`), event rows + labels, page rows with Markdown blocks |
  645. | `lib/projects.hl` | projects + members: `projects` / `members` tables, slugs (old ones keep resolving; `reservedSlugs`, ticket #25), roles use / edit / admin |
  646. | `lib/users.hl` | ticket #7: `users` + `tokens` tables, the ident exchange, sessions → users, the login env, `tagOf` |
  647. | `lib/invites.hl` | ident invites: `mayInvite`, `createInvite`, `joinByInvite` (the login callback's `invite=`) |
  648. | `lib/connections.hl` | ticket #21: an app connected to one project (`connections` table, request → code → key) |
  649. | `lib/markdown.hl` | ticket #6: Markdown text → blocks/spans (safe links); `components/markdown.hl` renders them |
  650. | `lib/mdview.hl` | ticket #6: the `Accept: text/markdown` documents (one ticket / a list; relations #4) |
  651. | `lib/migrate.hl` | the ticket #20 migration, idempotent, every start (the one file writing other topics' tables) |
  652. | `lib/import.hl` | the AntColony ticket-file format + `paragraphsOf` (import tool and fix tool) |
  653. | `lib/api.hl` | the function routes: JSON API, `/login/callback`, `/connect`, the old page URLs' 301s (`/tickets/:ref`, `/projects/…`) — thin wrappers |
  654. | `lib/api-helpers.hl` | replies, query/body (`readBody` = JSON check + strict fields), token / app-key auth, list filters, the Markdown Accept check, the small HTML page |
  655. | `lib/jsoncheck.hl` | hand-written JSON syntax check (workaround for hybriel #12 — delete with its one call once JSON.parse can fail softly) |
  656. | `lib/util.hl` | shared small helpers: env, the storage dir, URLs, `countOf` / `first` / `merged`, insertion sorts, text checks, Vienna time |
  657. | `components/` | the shell (`main.hl`) and the pages; `styles.hl` = all CSS (imports the tokens from `shared/tokens.hl`, sets the accent) |
  658. | `components/you.hl` | `/you`: account + API tokens |
  659. | `login.js` | the selector bridge (plain JS, served at `/login.js`) |
  660. | `shared/md-editor.js` | ticket #12: `<md-editor>`, vendored copy of worldapi-components (served at `/md-editor.js`) |
  661. | `shared/tokens.hl` | the WorldAPI tokens (webex `var()`), verbatim copy of worldapi-tokens/tokens.hl — README "Design tokens" |
  662. | `tools/import.hl` | the import command (needs `TICKETS_TOKEN`; section "Import") |
  663. | `tools/fix-import-summaries.hl`, `tools/migrate-short-ids.hl` | one-offs (2026-09-24 summaries; ident#23 short ids) |
  664. | `tests/browser.mjs` | the gate; `tests/connect.mjs` the connect gate; `tests/identkit.mjs` the gate's own ident |
  665. | `tests/oldstore.hl` | the gate's old-format fixture for the migration |
  666. | `tests/localtime.hl`, `tests/markdown.hl` | print Vienna renderings of DST edges / markdown.hl's blocks for `MD_INPUT` |
  667. | `tests/realdata-baseline.mjs`, `tests/realdata-compare*.py` | a cleanup answers the same: reads + a write sequence on a live-data copy (section "Test" → "Same output") |
  668. | `tests/shorturls-realdata.mjs` | ticket #25 on a live-data copy: every project + N tickets on the short and the old URLs (section "Test" → "Short URLs on real data") |
  669. | `tests/projectids-realdata.py` | mission 012 on a live-data copy: old vs new API byte for byte, slug vs id byte for byte (section "Test" → "Project ids on real data") |
  670. | `tests/letcount.py` | code-order counts: root `.hl` files, project.hl lines, `let` that should be plain |
  671. | `icons/` | antcolony mission 046: `icon.svg` (source), `icon-192/512.png`, `apple-touch-icon.png`, `favicon.ico` — README "PWA" |
  672. | `docker-compose.yml`, `deploy.sh` | Byrodin container; the deploy from Loreana (section "Deploy") |
  673. | `import/tickets/` | copies of the AntColony ticket files (source: byrodin) |
  674. ## History and worker briefs
  675. - `LOG.md` — append-only history, one dated line per step (moved here from the antcolony LOG on 2026-10-01).
  676. - `missions/NNN-*.md` — worker briefs for this app; `reports/NNN-*.md` — their reports (same name). Numbered per
  677. project since 2026-10-01 (antcolony#40); older text, code comments and commits use the old antcolony numbers →
  678. map: `/media/STORAGE/projects/antcolony-docs/docs/mission-map.md` (Byrodin: `/CONTAINERS/projects/antcolony/docs/mission-map.md`).

Branches

  • mainmain branch

Latest commits

  • bb64d57cmission 012 (ticket #25 API part): every /api/projects/<x>/… route takes the project's id (12-char record id) as well as its slug, same answers byte for byte; tickets stay under their project, no global /api/tickets/<x>. gate 315/0 (old code: the 13 id checks fail), connect 60/0, live-data copy old vs new 1,484 identical, slug vs id 772 identicalmre
  • 7538b034mission 011 (ticket #25 web part): short page URLs /<slug> and /<slug>/<number>; /projects/<slug>, /projects/<slug>/<n>, /projects/<slug>/tickets/<n> and /tickets/<ref> answer 301 (current slug); slug pages moved behind hl:web's own routes after construction (they answered /__hl/* and /components/*.hl); reserved slugs refused; API paths unchanged. gate 293/0, connect 60/0, live-data copy 187/0, API old vs new: only page links differmre
  • 0369106emission 010 (code order) 4/4: README file map + import order + 'Same output' test, STATUS (entry, lessons, how to verify), LOG, report; tests/realdata-baseline.mjs + realdata-compare*.py (a cleanup answers the same on live data), tests/letcount.pymre
  • a75e0279mission 010 (code order) 3/4: let only where a variable is reassigned or re-bound in a loop body (456 lets → plain declarations; Hybriel refuses a plain declaration inside a loop on its 2nd pass). gate 249/0, connect 60/0, real-data reads identical, a 50-step write sequence (API + faces) identical to the old codemre
  • e9d5c618mission 010 (code order) 2/4: one lib/ file per topic — store.hl split into projects / tickets (+ relations) / events / tickets-helpers, util.hl shared helpers (env, storage dir, URLs, sorts, Vienna time), the function routes out of project.hl into lib/api.hl (thin; auth/filters/Accept in api-helpers.hl), invite + member-removal logic out of the faces/routes into invites.hl / tickets.hl; project.hl is the map. /login/callback gets req + the session store by reference. gate 249/0, connect 60/0, real-data reads identicalmre
  • 97e269b5mission 010 (code order) 1/4: .hl files out of the root — lib/ (store, users, connections, invites, migrate, markdown, mdview, import = ticketfile, util = localtime, jsoncheck, api-helpers = api), tools/import.hl, components/styles.hl; import paths only. gate 249/0, connect 60/0, real-data reads identicalmre
  • 38f9d10ftickets: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); gate 249/0, connect 60/0mre
  • d3db6139tickets: Hybriel master 190aa11d (fc838894 GC correctness, #126 closure scopes, #127); gate 249/0, connect 60/0mre
  • bce182e3tickets: Hybriel master 7eea0d32 (#126 memory, #48 lambda copies its argument); migrate.hl lambdas take &logmre
  • 4137be0fantcolony#40: mission references point to the moved missionsmre
  • 9bfba36aantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
  • c7bd2645tickets: Hybriel master 73267707 (#122 fixed); compactNow workaround removed (#110 covered)mre
  • 2ab91ee9tickets: gate checks rows appear once (session sync); re-vendor to ff51cf46 stopped on hybriel#122, stays 837fe120mre
  • e01c2b1dtickets#24: installable app (manifest, service worker, offline list), own icon; gate waits for the hello's pongmre
  • 752fbb7fdeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • 38bdd5e4deploy.sh: never send .git or .gitignore to Byrodinmre
  • f12fa1bcState of 2026-09-27, before the move to gitoriamre