tickets
All repositories: gitoria
54.9 KB
# tickets.worldapi.orgThe World Ticket System: the only state store of AntColony (see byrodin`/CONTAINERS/projects/antcolony/README.md`). Projects, tickets, an append-only eventhistory per ticket (created / comment / state change), a web UI with live push, a JSONAPI for the scheduler/CLI, and an idempotent import of the AntColony ticket files.Written in **Hybriel** on **hl:web** (SSR + WebSocket push; hybriel master since antcolony mission 036), modelled on the hybrielrepo's `projects/demo-social-network`. **Login via ident** (ticket #7, `CONCEPT.md` "Login viaident"): reading is public; writing (tickets, comments, states) needs a login; the author isthe 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.See section "Login (ident)". **Markdown** in text fields and a Markdown read view for LLMs(ticket #6), **editing** by the ticket's author (ticket #8): section "Markdown, editing".**Relations** (ticket #4): parent / child and blocked-by, across projects — section "Relations".**Markdown editor** (ticket #12): every Markdown field is an `<md-editor>` — section "Markdown editor".## Run```bashcd /media/STORAGE/projects/tickets.worldapi.orgsetsid nohup ./bin/hybriel project.hl > server.log 2>&1 < /dev/null & echo $! > server.pid# stop: kill $(cat server.pid)```* Port **8350** on 0.0.0.0 (`TICKETS_PORT` moves it): http://192.168.178.75:8350 (LAN),http://100.77.141.84:8350 (tailnet), http://127.0.0.1:8350.* Data: `storage/mpackdb/{projects,tickets,events}.*` (hl:mpackdb, primary key = mpackdbUUID `@id` everywhere — creator's convention; `TICKETS_STORAGE=<dir>` moves them).Sessions: `.sessions/` (`TICKETS_SESSIONS`). The OLD global-numbered tables`storage/tickets-*` (before ticket #38) stay as a read-only backup — nothing reads them.* The framework's dev watcher is on: saving a `.hl` file re-analyses and reloads open tabs.A restart is needed after changing `styles.hl` root rules, `shared/tokens.hl`, env vars or the binary.* `HL_HOST` (or `HOST`): interface to bind, default 0.0.0.0; `127.0.0.1` on Byrodin. hl:webreads it itself (hybriel#24; until antcolony mission 036 `project.hl` built the listener). `TICKETS_WATCH=0` turnsthe dev watcher off (the container).* Login env (section "Login (ident)"): `IDENT_API_KEY`, `IDENT_API_SECRET`,`TICKETS_CREATOR_IDENTITY`, `IDENT_URL`, `IDENT_EXCHANGE_URL`, `TICKETS_PUBLIC_URL`. Withoutkey/secret the app runs read-only for everyone (a login answers "login is not set up").## Projects, members, roles, new states (ticket #20 — supersedes #19 and the creator-only workflow)- **Projects are records** (`projects` table): title, slug (generated from the title — letters, digits, `.`, `_`, `-`; an admin can changeit, the old slug keeps resolving), description (Markdown, the shared `<md-editor>`), members. Tickets point at their project by its **id**(never by name); `path` = `<project id>/<number>`. Pages: `/new-project`, `/projects/<slug>` (the tickets), `/projects/<slug>/settings`(admin: title, slug, description, members, invite link).- **Roles** (`members` table, one row per project + user): `use` = comment + move a ticket between open and review; `edit` = use + create, edit,assign, any state, relations; `admin` = edit + settings and members. A project always keeps at least one admin. Everybody else only reads.Any logged-in user with a display name may open a project and becomes its admin.- **Tickets** carry `createdBy` and `assignee` (members). **States**: open, progress, pending, review, reopened, done, canceled. A ticket thatgoes to review / pending with nobody assigned is assigned to the project's oldest admin (a second, `assign` event). The **inbox**(`/inbox`, `GET /api/inbox` with a token) shows what is assigned to you, as two lists: pending and review.- **API aliases**: the old state names are accepted as input (`in progress`, `awaiting creator`, `awaiting-creator`, `confirmed`, `rejected`, `on hold`)and mean progress / review / done / reopened / pending. Output uses only the new names.New endpoints: `GET/POST /api/projects`, `GET/POST /api/projects/:slug`, `POST /api/projects/:slug/members` (`{user, role}`; user = display name,ident id or user id), `POST …/members/remove`, `POST …/invites` (`{role, uses?, days?, email?}` → an ident invite link, ident#22),`POST /api/tickets/:ref/assign` (`{assignee}`), `GET /api/inbox`. A refused role → 403.- **Invites**: the person opens ident's link, picks an identity, ident sends them to `/login/callback?ident_code=…&invite=<id>`; only when identsays that identity accepted the invite (`/api/invites/get`) does the person join with the invite's role.- **Migration** (`migrate.hl`, idempotent, runs at every start): old projects `{name}` → records, tickets re-linked by id, states renamed(awaiting creator → review, a "Question…" ticket → pending, confirmed → done, rejected → reopened, in progress → progress, on hold → pending;review / pending tickets go to the creator), `createdBy` from the created event; members: the creator (TICKETS_CREATOR_IDENTITY, read ONLY here)is admin, users named Architect / AntColonyScheduler that wrote in a project are edit, everybody else who wrote is use. It compacts the tablesit changed (a second open of a table — the page realm — compacts on open and would rewrite the file under the first handle).- 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.- Gate: `tests/browser.mjs` (233 checks). The pre-roles browser checks (creator-only confirm, author-only edit, relations by author, tokens page …)are PARKED in `tests/browser-pre20-parked.txt` — they need porting to the roles.## Login (ident) — ticket #7* **How it works**: ident's identity selector top right (`<ident-selector key=IDENT_API_KEY>`from `<IDENT_URL>/selector.js`) plus a "Log in with ident" button(`<IDENT_URL>/login?key=…&return=<TICKETS_PUBLIC_URL>/login/callback`). Both hand tickets aone-time code; the SERVER exchanges it (`POST <IDENT_EXCHANGE_URL>/api/exchange` with key +secret) for the **per-app identity id**. Selector: `login.js` bridges the `ident-login` DOMevent into the shell's face `identLogin` — no reload, the pages show their forms (`signedIn`push to this session's tabs). Button: `/login/callback` sets the session and redirects back tothe page the login started from (ticket #10: at the click `login.js` adds`?next=<path + query>` to the button's return URL; the server follows only a same-originpath — one leading `/`, not `//`, no `\`, URL-safe chars, ≤ 500, not `/login/…` — else `/`).Logout (top right) = the shell's face `logOut`; the selector is reset (class `out` → `loggedIn = false`).* **`login.js` runs more than once per page** (ticket #9): hl:webex re-creates the header —the selector element AND the `<script>` elements — whenever the login state flips, and thebrowser executes a re-inserted script again. It therefore installs itself once per document(`window.__ticketsLogin`), listens for `ident-login` on `document`, always looks up theCURRENT `#selector`, and hands each one-time code to `#identcode` only once. (Before: one morelistener per run → after a logout one selection = two exchanges of the same code → red banner"ident refused the login (400: unknown or already used code)", although the first logged in.)* **Users** (`storage/mpackdb/users.*`, `users.hl`): one per identity id `{identity, name,created}`. The first login asks once for a **display name** (a normal field — ident may fill itlater, CONCEPT point 6); writing waits for it; it cannot be changed afterwards (no UI).The session holds only `user = { id = <users @id> }` (hl:webex ships `session.user` to thepage); the identity id is never in a page except the user's own `/you`.* **Authors**: new events store `user` (+ the name as `author`); the history shows the user'sdisplay name. Old events keep their free-text `author`. The web forms and the API have noauthor field (API: a body with `author` → 400 naming it).* **Creator**: `TICKETS_CREATOR_IDENTITY` = the creator's per-app identity id. Only that usermay set `confirmed` / `rejected` (web: message; API: **403** `{error, field:"state"}`).**Unset = nobody can.** To set it on Byrodin: the creator logs in to tickets, opens **`/you`**("Your tickets identity id", 32 hex), the architect puts `TICKETS_CREATOR_IDENTITY=<id>`into `/CONTAINERS/projects/tickets.worldapi.org/.env` and restarts the container.`/you` then says "You are the creator".* **API tokens** (`storage/mpackdb/tokens.*`): on `/you` a logged-in user creates (optionallabel), lists and revokes tokens. Shown ONCE (`tkt_` + 48 hex), stored as sha256 only.`Authorization: Bearer <token>` acts as that user on every API write; no / wrong / revokedtoken → **401** (checked before the body). Reads need no token.* **Env**: `IDENT_API_KEY` / `IDENT_API_SECRET` (tickets' registration in ident, origin`https://tickets.worldapi.org`), `TICKETS_CREATOR_IDENTITY` — all in `.env` on Byrodin (thehybriel runtime loads `.env` beside project.hl; the real environment wins; never commit it).`IDENT_URL` (default `https://ident.worldapi.org`: selector script + button),`IDENT_EXCHANGE_URL` (server side; default = IDENT_URL; `docker-compose.yml` sets`http://127.0.0.1:45002` = ident's loopback port on Byrodin), `TICKETS_PUBLIC_URL` (default`https://tickets.worldapi.org`: the button's return URL).* **ident down**: a login answers "ident did not answer" (the global `on Error` in project.hlabsorbs hl:fetch's failure — it logs `error absorbed: …` for every plugin error).## Markdown, editing (tickets #6, #8 — mission 007 (old 014))* **Markdown in text fields** (summary, comment text, state note): `markdown.hl` parses the textinto plain data (blocks → spans), `components/markdown.hl` builds elements from it — everycharacter ends up in a TEXT node, no HTML string exists, so raw HTML / `<script>` shows as text.Links only when `safeHref` accepts them: `http(s):`, `mailto:`, `/path` (not `//`), `#…`;`javascript:`, `data:`, `vbscript:` etc. stay text. Understood: `#`…`######` headings (shown ash3/h4/h5), `-`/`*`/`+` and `1.`/`1)` lists (one level; a list may interrupt a paragraph), fencedcode (``` / ~~~), `code`, `[text](url)`, `<https://…>`, bare http(s) URLs, `*em*`, `**strong**`,`***both***` (also `_`), `\` escapes. Single line breaks inside a paragraph are KEPT (the pageshowed them before). Not: quotes, tables, images, nested lists. Only pages and pushes carry theparsed blocks (`md`, `summaryMd`, `oldSummaryMd` via `store.hl pageEventOf`); **API JSON unchanged**.* **Read view for LLMs**: `Accept: text/markdown` on `GET /api/tickets`, `/api/projects/:slug/tickets`,`/api/tickets/:ref`, `/api/projects/:slug/tickets/:number` → `text/markdown` (+ `Vary: Accept`),built by `mdview.hl`: one ticket = `# <project> #n: <subject>`, a meta line, URL, the summary aswritten, `## History` with one `### <seq> · <when> · <author> <what>` per event (texts below, anedit shows "Subject before:" / "Summary before:" quoted); a list = `# Tickets (<filters>): N` +one line per ticket. Markdown wins only if it is listed with q > 0 and preferred to`application/json` (higher q, or same q and first); `*/*` / none → JSON. Errors stay JSON.`curl -s -H 'Accept: text/markdown' http://127.0.0.1:8350/api/projects/tickets.worldapi.org/tickets/6`* **Editing**: the ticket's AUTHOR (the user of its `created` event) edits subject + summary —web: "Edit" on the ticket page (form, Save/Cancel), API: `POST …/edit { subject?, summary? }`.**A ticket from before the login** (created event without a user, only an author string):**only the creator** (`TICKETS_CREATOR_IDENTITY`) may edit it. Anyone else: web message, API**403**. The edit is a new history event `kind:"edit"` with `oldSubject`/`oldSummary` +`newSubject`/`newSummary` (+ `subjectChanged`/`summaryChanged`, `isEdit`) — earlier versions stay(append-only); pushed live like a comment (the page takes over subject + summary). The pageshows "X edited the ticket", "Subject before" (struck through) and the previous summary folded.No migration: only NEW events have the edit fields; existing JSON is byte-identical (mission 007 (old 014)`.scratch/m014/compat.sh`).* **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 aninline `<script>` without escaping `</script>` → a summary/comment with`</script><img src=x onerror=…>` RAN in every viewer's browser (stored XSS, reproduced on thepre-m014 code: `node .scratch/m014/xsscheck.mjs <url>` → `__pwned = 1`). Fixed in the vendored`plugins/webex/WebFramework.hl` (seed: every `<` written as `\u003c`); to be filed as a Hybriel issue (mission 007 (old 014) report).The gate's `</script>` check (MD_RICH) still guards it.## Markdown editor (ticket #12 — missions 009 (old 024) + antcolony 025)* The new-ticket summary, the edit summary, the comment and the state note are `<md-editor>`saround their textareas (`mdEditor { textarea { … } }`): edited visually (formatted while typing,toolbar incl. phones, shortcuts), the textarea keeps the **plain Markdown** and fires `input` asbefore — members, faces, API and stored data are unchanged. The state note is now a textarea(rows 1): the editor writes line breaks. Ctrl/Cmd+Enter sends the form.* A "Markdown source" button in a footer row at the bottom of every editor (antcolony mission 025, creator'sfollow-up request) switches it to a plain textarea with the raw Markdown, to copy it out or editit by hand; "Visual editor" switches back (re-parses the typed text). Same `input`/`change`events and Ctrl+Enter as the visual editor.* The component is **vendored**: `shared/md-editor.js` = verbatim copy of`/media/STORAGE/projects/worldapi-components/md-editor.js` (edit it THERE, test it there, `cp` ithere; `cmp` them). Served at `/md-editor.js` (project.hl), loaded once in the shell (main.hl).Its README: attributes, keyboard, lossless rules, restyling. Colours: `mdEditor { '--md-…' = token }`in `styles.hl`.* Without JS the page shows the plain textareas (SSR); the forms themselves need JS as before(webex faces).* Only tickets' Markdown subset exists in the editor (its parser is a port of `markdown.hl`, checkedagainst it by the component's test with `ORACLE_APP`); pasted rich text is reduced to it; anuntouched text comes back byte for byte. **Keep `markdown.hl` and md-editor.js's parser in step.**## Relations: parent / child, blocked by (ticket #4 — mission 008 (old 017))* **Parent / child**: a ticket has at most one parent (any project). The parent's page lists itschildren with their states ("Children"); a child says "Part of <parent>". **Blocked by**: a ticketcan wait on any number of tickets (any project); the blocked page lists "Blocked by", the otherside "Blocks" — each with its state.* **Parent state** (smallest rule): `allChildrenConfirmed` = the ticket has children and every childis `confirmed`. The page shows "all children confirmed (n)" in green, else "k of n childrenconfirmed". The parent's own state is NEVER changed by it — only the creator confirms.* **Who may set / remove a relation**: the AUTHOR of either of the two tickets (user of its createdevent) or the creator (`TICKETS_CREATOR_IDENTITY`). A ticket from before the login has no author:only the creator or the author of the other ticket. Replacing a parent needs the right for the oldAND the new pair. Refused: a ticket as its own parent / blocker, loops (a parent below the ticket,a blocker that already waits on it, also transitively), duplicates, unknown tickets.* **Naming a ticket**: `<project>#<number>` (spaces around `#` allowed, e.g. `ident.worldapi.org#1`)or its UUID.* **Storage**: a NEW table `storage/mpackdb/links.*` (`{ kind: 'parent'|'blockedBy', ticket, other,user, created }`, indexes `@ticket`, `@other`), created empty at the first start — no migration. Aremoved relation is deleted there; the HISTORY keeps every change: an event `kind:"link"` on thechild / the blocked ticket ("set the parent to X #1 (was Y #2)", "removed the parent X #1", "markedit blocked by X #5", "removed the blocker X #5"; extra JSON keys `isLink, linkKind ('parent' |'blockedBy'), linkAction ('set' | 'removed'), otherRef, otherHref, previousRef` — only on link events).* **JSON**: every ticket row (list AND single GET, pushes) has `parent` (a ref or `null`), `children`,`blockedBy`, `blocks` (lists of refs, oldest link first) and `allChildrenConfirmed`. A ref =`{ id, project, number, ref ('#1'), key ('ident.worldapi.org#1'), subject, state, stateSlug, href,apiHref }`. All other keys unchanged (mission 008 (old 017) `.scratch/m017/compat.sh`).* **Markdown view**: under the URL line `Parent: <p> #n [state] subject · url`, `Children (k of nconfirmed):` / `Children (all n confirmed):`, `Blocked by:`, `Blocks:` (one `- <p> #n [state]subject · url` line each); a list line ends with ` · parent … · children n, k confirmed | allconfirmed · blocked by <p> #n [state], … · blocks …` (only the parts that exist).* **Web** (logged in): under "Respond" the forms "Parent ticket (project#number)" → Set parent /Remove parent, "Blocked by (project#number)" → Add blocker; a "Remove" button per blocker row.Faces `ticketSetParent(id, key)` ('' = remove), `ticketAddBlocker`, `ticketRemoveBlocker`.* **Live**: every relation write, state change and edit pushes `ticketsRelated(views)` — the relationview of the ticket and of every ticket related to it (+ one just unlinked); an open page of any ofthem follows without a reload (a child confirmed → the parent's "k of n" moves).* **For the scheduler**: parent tickets = rows with `children.length > 0` (don't pick them as work);a ticket waits while any `blockedBy[].state` is not what the scheduler counts as done. A report's`issues[].blocks = true` → create the issue ticket, then `POST <original>/blocked-by {"add":"<project>#<n>"}` with the scheduler's token (it is the issue's author, so it may).## PWA — installable app, icons, offline shell (antcolony mission 046)Done the way calendar.worldapi.org does it: hl:web generates everything from `project.hl` — no JavaScript of ours.- `appTitle` "tickets", `appIcons` (192/512, `any` + `maskable`, same PNGs), `appTouchIcon`, `appFavicon`,`appThemeColor` = the header's `darker` `rgb(15, 20, 25)` (as tracker), `appBackgroundColor` = dark `rgb(25, 30, 35)` (both from `shared/tokens.hl`).Served: `/__hl/manifest.webmanifest` (linked from every head with apple-touch-icon + theme-color), `/__hl/sw.js`.- `offline = [ TicketList ]`: `/` is the ticket list, so offline it is the list as this browser last loaded it (the workerprecaches `/` at install; a visited `/projects/<slug>` is kept too). Every other page offline → hl:web's "Unavailableoffline" (503). TicketList's emits are all value-form, so nothing is queued offline.- "You are offline" note in the shell (`components/main.hl` `offline`, `#offline`): hl:web has no connection state and nomount hook, so an invisible `<net-probe>` runs an endless 1 s CSS animation (`styles.hl` `tickets-net-tick`) and every`animationiteration` reads `navigator.onLine` (same trick as tracker).- Icons `icons/`: `icon.svg` is the SOURCE (a tilted ticket stub, purple on dark, inside the maskable safe circle r=205).Rebuild after editing it:```bashcd icons && for s in 192 512; do rsvg-convert -w $s -h $s icon.svg -o icon-$s.png; donersvg-convert -w 180 -h 180 icon.svg -o apple-touch-icon.pngfor s in 16 32 48; do rsvg-convert -w $s -h $s icon.svg -o /tmp/fav-$s.png; donemagick /tmp/fav-16.png /tmp/fav-32.png /tmp/fav-48.png favicon.ico```Each icon has its own `file` route in `project.hl` (`/favicon.ico` serves `icons/favicon.ico`).- Test: the gate's last part (browser C) — see "Test (the gate)".## Deploy (Byrodin)Target: `/CONTAINERS/projects/tickets.worldapi.org` on Byrodin, container `tickets.worldapi.org`(`docker-compose.yml`: debian:12-slim, host network, `HL_HOST=127.0.0.1`, `TICKETS_PORT=45003`,`TICKETS_WATCH=0`, the folder mounted at `/home/tickets`, `./bin/hybriel project.hl`), publichttps://tickets.worldapi.org/ via nginx (TLS ends there; the vhost needs the WebSocket Upgradeheaders — the live push rides `/__hl/socket`; no baseUrl/tls in the app, like notes).* **First deploy: done by the architect** (folder + data, nginx vhost, cert, DNS).* **Later: `./deploy.sh`** on Loreana, in this folder: runs the gate (refuses on a failure;`--skip-tests` skips it LOUDLY), backs up `storage/`/`.sessions/`/`.env` (what exists) toLoreana's `/media/SLOW1TB2/deploy-backups/<app>/` (newest 5 kept; an empty/failed backup stopsthe deploy), then rsyncs the code to`[email protected]:/CONTAINERS/projects/tickets.worldapi.org` (never `storage/`, `.sessions/`,`.env`, `.scratch/`, `server.*`, `testapp/`, logs — the preview is checked for them; no`--delete`), `docker compose up -d && docker compose restart` over `ssh -F /dev/null`, thenwaits for https://tickets.worldapi.org/ to answer 200. Every step is printed.* `./deploy.sh --dry-run` = gate + `rsync -n` + the commands it would run (no restart, no URLcheck). `--target DIR|HOST:DIR` / `--url URL` point it elsewhere (tested only against a localdirectory, ident STATUS "ident mission 006 (old 010)").* Import on Byrodin: `TICKETS_URL=http://127.0.0.1:45003 TICKETS_TOKEN=tkt_… ./bin/hybriel import.hl` (in the containeror on the host with the vendored binary).* Session cookie `hlsid` (host-only; tickets has its own host on Byrodin). Login: `.env` needs`IDENT_API_KEY`, `IDENT_API_SECRET`, later `TICKETS_CREATOR_IDENTITY` (section "Login (ident)").No migration: `users.*` / `tokens.*` are created empty at the first start; old events keeptheir authors (checked mission 005 (old 011): every GET of old vs new code on a copy of the store identical).## Import the AntColony ticket files```bash# on byrodin — refresh the copies:scp /CONTAINERS/projects/antcolony/tickets/*.md loreana:/media/STORAGE/projects/tickets.worldapi.org/import/tickets/# on loreana, with the server running (an API token of the user who should open them, /you):TICKETS_TOKEN=tkt_… ./bin/hybriel import.hl # TICKETS_IMPORT_DIR (default ./import/tickets), TICKETS_URL (default http://127.0.0.1:8350)```Format: `project:` and `subject:` header lines, blank line, summary (`ticketfile.hl`). Thesummary is hard-wrapped text: its lines are joined into paragraphs (one space), a blank linestarts a new paragraph; a Markdown list line (`- `, `* `, `1. ` after trimming) starts a newline (`\n`) and following ordinary lines join onto it (wrapped item; nesting indentation isdropped); stored as paragraphs separated by `\n\n`, shown with `white-space: pre-wrap`. CRLF files work. Each file is POSTed tothe RUNNING server (`/api/tickets` with `source` = file name); a known source answers theexisting ticket, so re-running prints `0 new, N already there`. New tickets are numberedper project in sorted file-name order (output: `NEW 0004-scheduler.md → antcolony #1`).## Ticket numbers, URLs, the migration (ticket #38)* A ticket's key is the mpackdb UUID (`id`, e.g. `0mufbphip3w7`). People use **project +number**: numbers count per project from 1 (next = highest in the project + 1).* **Slug rule: the project slug IS the project name, as is.** A new project name may only holdletters, digits, `.`, `_`, `-` (all existing names do), so it goes into URLs unescaped.* Ticket page: **`/projects/<project>/<number>`** (e.g. `/projects/tickets.worldapi.org/5`).`/projects/<project>` = the project's list (same as `/project/<project>`).* **Old global numbers** (`#1`…`#39` in docs and comments) are kept on the migrated ticketsas `oldNumber`; the ticket page shows "formerly #38"; **`/tickets/<old n>` → 301** to the newURL (also `/tickets/<uuid>`); `/api/tickets/<old n>` keeps answering. New tickets get no old number.* Ordering never uses key order: lists by stored `updated`, a history by (`created`, `seq`),projects by `created`.* Migration 2026-09-24 (`tools/migrate-008.hl`, idempotent, header explains it): old #N →`<project> #k` by creation order. Mapping printed in `.scratch/deploy-008-<ts>/migrate-run1.txt`;or ask the API: `curl -s localhost:8350/api/tickets/38 | jq .ticket.href`.## Test (the gate)```bashnode tests/browser.mjs # 423 checks, ~2 min; own server on :8352, own ident on :8353,# exchange-counting proxy on :8357 (TICKETS_GATE_EXCHANGE_PORT),# own storage in .scratch/gate-store (ident's code copied WITHOUT# .env to .scratch/gate-store/ident, mail to a sink — never real mail)```**Ports** (mission 008 (old 017)): `TICKETS_GATE_PORT`, `TICKETS_GATE_IDENT_PORT`, `TICKETS_GATE_EXCHANGE_PORT`,`TICKETS_GATE_CHROME_A` / `TICKETS_GATE_CHROME_B` (`"8810-8814"`; default 8620-8629 / 8630-8639) — aworker with a port range runs e.g. `bash .scratch/m017/gate.sh <out.txt>` (8800-8819).`TICKETS_GATE_CHROME_C` (default 8640-8649) is the PWA browser. antcolony Mission 046 ran it as`TICKETS_GATE_PORT=8750 TICKETS_GATE_IDENT_PORT=8751 TICKETS_GATE_EXCHANGE_PORT=8752 TICKETS_GATE_CHROME_A=8753-8754TICKETS_GATE_CHROME_B=8755-8756 TICKETS_GATE_CHROME_C=8757-8759 node tests/browser.mjs` → 245 passed (~30 s).antcolony Mission 046 part (**PWA**, at the very end, own fresh Chrome C): head links manifest / apple-touch-icon / favicon /theme-color; manifest fields; every icon a real PNG of its size; favicon.ico + icon.svg; the worker registered withscope `/` and controlling; `Page.getInstallabilityErrors` empty; then the SERVER IS STOPPED and the page set offline(CDP) → reload `/` shows header + last list + `#offline`; `/inbox` → "Unavailable offline"; online again → the note goes.Screenshots `.scratch/gate-pwa-phone-online.png` / `gate-pwa-phone-offline.png` (390 px) — LOOK at them.**Ident for the gate**: the gate runs ident's CODE from `../ident.worldapi.org` (or `TICKETS_GATE_IDENT_DIR`). On 2026-10-01 ident wasbeing re-vendored to hybriel master and its own sign-in broke (`ident /code page` timeout at browser.mjs:552; ident's own gate failsthe same way). Tickets' gate then ran against a snapshot of ident's code with ident's 837fe120 bin/plugins (what live ident runs):`.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.antcolony Mission 048 "**once:**" checks: after a comment, a state change and adding a member, the new row is in the list exactly once(hybriel 64527baa+ answers a session face with a sync of session-derived members — a handler appending the row too would double it).`connected(page)` in the gate waits until the server has READ the tab's `hello` (a ping on the same socket, its pong),not just an open socket: a push sent before that reaches no component (the inbox-live check failed ~1 in 5 without it).Mission 009 (old 024) part (**#12 Markdown editor**, before the console check): bob on his ticket — SSR has theplain textareas inside `<md-editor>`, `/md-editor.js` served, editors upgraded; a comment by REAL typing(`**x**`, `` `x` ``, `- ` list) sent with Ctrl+Enter → stored Markdown + rendered + editor cleared; astate note with the toolbar "B"; a hostile rich paste (img onerror, script, javascript: link) reducedto the subset, sent, rendered safely; the edit form opens the stored summary formatted and byte forbyte, typing at the end changes only that block, saved; at 390px the new-ticket form with toolbartaps (inline code) → stored. Screenshots `gate-{phone,desktop}-mdeditor.png`, `gate-phone-newticket-mdeditor.png`.The component's own test (72 checks): `worldapi-components/test/run.mjs`.Mission 008 (old 017) part (**#4 relations**, after #8/#6): project `rel.example` — the gate user opens P (#1)and C1–C3 (#2–#4), bob X (#5). API: set parent 3 ways (per-project, UUID route + UUID, `rel.example # 1`),P's children + states, list rows carry the relations, row keys = old + exactly the 5 new, 14 refusedparent writes (401, 403 ×2, itself, unknown, no `#`, loop, same, none to remove, `{}`, unknown field,non-string, author, 404) write nothing; who: bob (author of the child) sets/removes, a legacy ticket →403 for bob, 201 for alice (creator); replace (label "(was …)"), a grandchild loop. Blocked-by: add,13 refusals (401, 403, itself, twice, direct + 2-step loop, add+remove, `{}`, empty, unknown, notblocking, unknown field, 404), remove; non-link events keep the old keys. Markdown: child"Parent:" + "Blocked by:", parent "Children (0 of 3 confirmed):", blocker "Blocks:", list-line tails.Browsers: B (bob) on P sees 3 children + "0 of 3"; signed out: relations yes, forms no; A (alice) on C2"Part of …", "Remove parent" → B drops to 2 live, typed "Set parent" → 3 again, unknown parent →message; B on X "Blocks", A "Add blocker" → both live, bob's face refused, 3 forged faces refused, therow's "Remove"; alice confirms C1, C2 (API) → B "2 of 3", C3 (web) → "all children confirmed (3)" green,P stays open. Screenshots `gate-{phone,desktop}-{parent,blocked}.png` — look at them.Mission 007 (old 014) parts: **#8** — the gate user opens gamma #3 with a Markdown summary, edits subject(per-project URL) and summary (`/api/tickets/<uuid>/edit`), history created/edit/edit with the oldvalues; 9 refused edits (no token 401, bob / alice (creator, not author) 403, `{}`, empty subject,same values, unknown field, non-string, author → 400) write nothing; 404s; legacy alpha #1: gate /bob 403, alice (creator) 201. In Chrome: bob's ticket (gamma #4) — "Edit" only for bob (notalice, not signed out), the form (screenshots `gate-{phone,desktop}-edit.png`,`gate-desktop-editbutton.png`), Cancel writes nothing, an empty subject is refused, Save → bothbrowsers show the new subject + rendered summary + "bob edited the ticket" with the previousvalues, no reload (`gate-{phone,desktop}-markdown.png`); faces: alice refused, forged sessionrefused; legacy alpha #1: "Edit" for alice only; logout hides it, a selector login brings it backwithout a reload. **#6** — JSON unchanged (non-edit events keep exactly the old keys;`Accept: application/json` = no Accept); the read view (ticket, uuid form, project list, statelist, 7 Accept variants, errors stay JSON); the parser table (`tests/markdown.hl`); the renderedsummary in Chrome (headings, em/strong, code, lists, code block; ONLY the 3 safe links; noscript/img/handler element; raw HTML incl. `</script><img …>` shown as text; `window.__xss` unset);a Markdown comment pushed live to both, a Markdown state note. **#11** — every page wait is`TICKETS_GATE_SLOW`× (default 3) its timeout; the console check runs BEFORE ident is stopped,both browsers park on `about:blank` first, then "nothing new in the console" after.Load proof (mission 007 (old 014)): `node .scratch/m014/load.mjs <tickets url> 4 6` (4 headless Chromes ×6 tabs looping pages, ports 8700-8719, SIGTERM closes them) while the gate runs.Ticket #7 part (login via ident, `tests/identkit.mjs`): our own ident from`../ident.worldapi.org` (or `TICKETS_GATE_IDENT_DIR`), accounts alice / bob / gate, ticketsregistered with origin `http://127.0.0.1:8352`, alice's per-app id computed (selector code +exchange). Server #1 WITHOUT a creator: the machine user "gate" logs in through`/login/callback` (no / unknown / reused code → 400), the name prompt, the name rules (empty,61 chars, asked once), a token over the face, `/you` shows its id, confirm / reject → 403.Server #2 with `TICKETS_CREATOR_IDENTITY` = alice: the session survives the restart; every APIwrite with the gate token, `author` in a body → 400, 14 unauthenticated cases → 401 (auth beforethe body), reads without token. Browsers: signed out = history + hint, no forms, a face writerefused, screenshots `gate-{phone,desktop}-signedout.png`; A = alice signs in to ident, then theSELECTOR (no reload, `logged-in`, name prompt `gate-*-name.png`, forms appear,`gate-*-loggedin.png`); B = bob via the LOGIN BUTTON (ident /signin → /login/callback → name);writes show alice / bob / gate as authors; bob's confirm / reject refused (web + API 403), aliceconfirms; bob's token on /you (shown once, `gate-*-tokens.png`, not after reload) → API write asbob → revoke → 401; alice cannot revoke bob's token; 8 faces with a forged session (#31) refused;old events keep "creator" / "worker"; logout resets the selector without a reload; ident stopped→ "ident did not answer" (400, no 500). Ident's log: `.scratch/gate-ident.log`.Ticket #9: tickets reaches ident's `/api/exchange` through a proxy in the gate that COUNTS theexchanges. alice gets a 2nd identity "alice two"; on a ticket page: logged in → Log out →selector → "alice two": no `#loginerror`, exactly ONE exchange (200), no reload, `/you` showsalice two's per-app id; then Log out → in-app click to the list → selector → "Default": againone exchange, logged in as alice. Ticket #10: `/login/callback?next=` table (16 cases: pathskept, `//host`, `https://`, `/\host`, `javascript:`, CR/LF, quotes, `/login/…`, 501 chars → `/`);in Chrome bob logs out, clicks to a ticket (pushState URL), "Log in with ident" → ident → backon that ticket, logged in; same from `/state/open`.Manual repro of #9 with instrumentation (listeners, `change`s, socket frames, exchanges):`node .scratch/m012/repro.mjs [appDir]` (`SCEN=ticket-nav|list|ticket-direct`, `SECOND=<identity>`;ident :8363, proxy :8366, tickets :8362, Chrome 8640-8649; output `.scratch/m012/repro-run/out.txt`).Ticket #38 part (runs first): `tests/oldstore.hl` writes an OLD-format store (5 tickets,old #1–#5 in `alpha` / `beta.example.org`, a same-ms tie), `tools/migrate-008.hl` migrates acopy of it TWICE (second run must print `0 new`), the server runs on the migrated tables:old-number and per-project API (GET + POSTs), `/tickets/<n>` 301 (fetch and in Chrome),"formerly #n", a real click on a list row, new tickets continue the numbering (alpha #4,new project gamma #1), pushed rows carry `/projects/…` hrefs (list + inbox in browser B).Screenshots also `gate-{phone,desktop}-legacy.png`.It starts its own server, runs the import twice, drives the API, and opens TWO headlessChromes (debug ports 8620-8629 / 8630-8639, `--disable-gpu`, killed by PID at the end):filters by real clicks, a comment and state changes in browser A appear live in browser B(no reload — checked with a window marker), API writes appear live in both, the inbox andheader count follow, the new-ticket form, and the layout at 390px and 1280px (no horizontaloverflow; screenshots `.scratch/gate-{phone,desktop}-{list,ticket,inbox}.png`,`gate-phone-newticket.png`, `gate-phone-paragraphs.png` — look at them). Also: the strict-APItable (26 refused bodies, nothing written), Vienna time against node's `Intl`(`bin/hybriel tests/localtime.hl` prints DST-edge samples), and a CRLF paragraph fixtureimported from `.scratch/gate-import` (→ #7). Ticket #15 (author cases replaced by ticket #7's "no author field"), 13 invalidJSON bodies → 400 at the right character and no source path, valid escapes still accepted,no name field in either form, a list fixture `.scratch/gate-import-lists` (→ #8) rendered asseparate lines (`gate-phone-lists.png`). Server log: `.scratch/gate-server.log`.Chrome: `HL_CHROME` (default `/opt/google/chrome/chrome`, google-chrome is not on PATH).Check for leaked Chromes afterwards: `ps -eo pid,args | grep [h]l-browser-tier` (must be empty).## Pages| Route | Component | ||---|---|---|| `/` | `components/ticket_list.hl` | all tickets, newest update first; project + state filter chips; "New ticket" form || `/project/:projectName`, `/state/:stateSlug`, `/project/:p/state/:s` | same | filtered (state slug: `in-progress`, `awaiting-creator`, `on-hold`, …) || `/projects/:projectSlug/:ticketNumber` | `components/ticket.hl` | ticket, history, comment form, state change (select + optional note), "Edit" for the author (#8); texts rendered as Markdown (#6) || `/projects/:projectName` | `components/ticket_list.hl` | = `/project/:projectName` || `/tickets/:ref` | function route | 301 → `/projects/<p>/<n>` (`:ref` = old global number or UUID), else 404 || `/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) || `/you` | `components/you.hl` | the logged-in user: display name, own per-app identity id, creator or not; API tokens (create / list / revoke) || `/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) || `/login.js` | file | the selector bridge (`ident-login` → the shell's hidden `#identcode`, once per code) + `next=` on the login button |Shell: `components/main.hl` (header, Inbox link with live count, top right the login: selector +button, or name link to `/you` + Log out; the display-name prompt). All CSS: `styles.hl`(tokens imported from `shared/tokens.hl` — section "Design tokens"; accent `colorAccent = var(purple)`= #c586c0, `--color-danger: var(--red)` = #f44747; domain tags `<ticket-board>`, `<ticket-state>`, `<ticket-view>`,`<event-head>`, …; mobile first, one `min-width: 45rem` block).Live push (audience in `project.hl`): `ticketCreated(row, inboxCount)` and`ticketEvent(ticketId (UUID), event, row, inboxCount)` (public), raised by the web faces AND theAPI; `signedIn(tag, {name, named, creator})` / `signedOut(tag)` only to the tabs of the sessionwhose login carries that random tag (`session.data.tag`).## API (JSON)`:ref` = the OLD global number of a migrated ticket (`38`) or the ticket's UUID (`id`).`:slug` = project name, `:number` = per-project number. Both forms answer the same shapes.| Method + path | Body | Answer ||---|---|---|| `GET /api/tickets[?project=&state=]` | — | `{ tickets: [row] }` (state as `awaiting creator` or slug) || `POST /api/tickets` | `{ project, subject, summary?, source? }` | 201 `{ ticket, existed:false }`; known `source` → 200 `{ ticket, existed:true }` || `GET /api/tickets/:ref` | — | `{ ticket, events: [event] }` (oldest first); 404 || `POST /api/tickets/:ref/comments` | `{ text }` | 201 `{ ticket, event }` || `POST /api/tickets/:ref/state` | `{ state, text? }` | 201 `{ ticket, event }`; 400 unknown/same state; 403 confirmed/rejected by a non-creator || `GET /api/projects/:slug/tickets[?state=]` | — | `{ tickets }`; 404 unknown project || `POST /api/projects/:slug/tickets` | `{ subject, summary?, source? }` | as `POST /api/tickets` || `GET /api/projects/:slug/tickets/:number` | — | `{ ticket, events }`; 404 || `POST /api/projects/:slug/tickets/:number/comments` | `{ text }` | 201 `{ ticket, event }` || `POST /api/projects/:slug/tickets/:number/state` | `{ state, text? }` | 201 `{ ticket, event }`; 403 as above || `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 || `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) || `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) |Every ticket GET answers `Accept: text/markdown` with a Markdown document (section "Markdown, editing").**Every POST needs `Authorization: Bearer <token>`** (ticket #7; a token from `/you`): missing,malformed, unknown or revoked → **401** `{ error }` + `WWW-Authenticate: Bearer`, checkedbefore the body. The author is the token's user. GETs need nothing.| `GET /api/projects` | — | `{ projects, states }` (projects in creation order) |Ticket row: `id` (UUID), `project`, `number`, `ref` (`#5`), `href` (`/projects/<p>/<n>`),`apiHref`, `oldNumber` (only if migrated) + `oldRef`/`hasOld`, `subject`, `summary`,`state`, `stateSlug`, `source`, `created`/`updated` (+`Ms`), `events`, `projectHref`.Event row: `id` (UUID), `ticket` (UUID), `project`, `number`, `seq`, `kind`, `author`,`text`, `from`, `to`, `when`, `createdMs`, …**Since #38 `ticket.id` is a UUID, not a number** — scripts that did `/api/tickets/${ticket.id}`keep working (UUIDs are accepted); for display use `project` + `ref`.**Strict** (ticket #10, `bodyError` in `api.hl`): a POST body must be a JSON object; an unknownfield, a missing or empty (after trim) required field, or a non-string value (null included)→ **400 `{ error, field }`** naming the field, and nothing is written. Semantic refusals(unknown/same state, bad project name) also carry `field`. `author` is gone (ticket #7): a bodythat has it → 400 `field:"author"` ("no field 'author' any more: the author is the user of yourAPI token"). Invalid JSON → **400 `{ error: "invalid JSON atcharacter N" }`** (no `field`, no source path): `jsoncheck.hl` checks the syntax BEFORE`JSON.parse` — a WORKAROUND for hybriel #12 (no soft parse); remove it (and `readBody`'s callin `api.hl`) once #12 is fixed. A LONE `\uD800`–`\uDFFF` escape is refusedtoo, because hl's `JSON.parse` aborts on it; a valid pair (`\ud83d\ude00`, Python's default`ensure_ascii=True`) is accepted since hybriel#15 (antcolony mission 036). Other errors: `{ error }` with 404/405.**Times**: `created` / `updated` / `when` are Europe/Vienna wall time `YYYY-MM-DD HH:MM`(`localtime.hl`: EU DST rule, correct from 1996 on — hl:time has no time zones);`updatedMs` / `createdMs` and the stored values are epoch ms (UTC). Example:```bashcurl -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/statecurl -s http://127.0.0.1:8350/api/tickets/38 | jq '.ticket | {project, number, href, oldNumber}' # old number still works```Query the store directly: `curl -s http://127.0.0.1:8350/api/tickets | jq` (the mpackdb filesare binary; the API is the query tool).## Connecting an app to a project (ticket #21)Another app (first: gitoria) is connected to ONE project; it then gets a key that reaches that project only. Code: `connections.hl`,`components/connect.hl`, the connections line on the project page (`components/ticket_list.hl`), routes in `project.hl`. Data:`storage/mpackdb/connections.db` (created empty at the first start; no migration).1. The app sends the browser to `GET /connect?app=<name>&label=<owner/repo>&return=<url>&state=<opaque>` (`label` and `state` optional; the statecomes back unchanged). The return URL must be https on `worldapi.org` or a subdomain — or start with an origin listed in the env`TICKETS_CONNECT_ORIGINS` (comma separated, for dev copies). Else 400. A valid request is stored (1 hour) and the browser goes to `/connect/<nonce>`.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 itsadmin) and confirms. The page says which app and host ask, then links back: `<return>?code=<one-time code>&state=<state>` (code valid 5 minutes).A second connection of the same app + label to the same project replaces the first (its key dies).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,shown once, only its sha256 is stored. Wrong / used / expired code → 400.4. The key: `Authorization: Bearer tktc_…` plus, on every write, `X-Tickets-Identity: <ident public id of the person>`. It works only on`POST /api/projects/<slug>/tickets` and `POST /api/tickets` (create), `…/comments` and `…/state` of tickets of ITS project — the sameendpoints 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 havelogged in to tickets once and chosen a display name; **the project's roles apply to them** (use: comment, open ↔ review; edit / admin: more).The author of what is written is that person. A key on any other endpoint (settings, members, invites, edit, assign, relations, inbox,new project, disconnect) → 401; another project → 403; no / unknown identity → 403 naming the header. Reads need nothing.5. The project page shows "Connected to <app>: <label>" to everybody; an admin's **Disconnect** deletes the connection — the key is dead at once.`GET /api/projects/<slug>/connections` lists them; `POST /api/projects/<slug>/connections/remove {"id"}` (user token, admin) disconnects.Short copy for app workers: `docs/connect-apps.md` (the architect copies it next to antcolony-docs).Gate: `node tests/connect.mjs` (ports 8700 / 8701 + Chrome 8703–8709; an ident stub, a real headless Chrome) → 60 checks.## Design tokens (shared, ticket antcolony#3 — antcolony mission 021)* **`shared/tokens.hl`** declares the WorldAPI palette + semantic tokens as hl:web css variables,hand-written: `static dark = var('rgb(25, 30, 35)')`, `static colorText = var(light)`, …(`import { var } from 'hl:web/css'`). It is a copy of the one source`loreana:/media/STORAGE/projects/worldapi-tokens/tokens.hl` (vendored like `plugins/`, no generator):edit it THERE, then `cp /media/STORAGE/projects/worldapi-tokens/tokens.hl shared/tokens.hl` in everyapp and restart it. Check: `cmp /media/STORAGE/projects/worldapi-tokens/tokens.hl shared/tokens.hl`.(2026-09-26: the apps' copy = gitoria's/ident's; it differs from the source only in 3 COMMENT lines thatstill say webex — update the source's comments, then the check is byte-exact again.)* `styles.hl` IMPORTS the tokens it uses (`import { colorText, colorBorder, … } from './shared/tokens.hl'`)and writes them as members: `color = colorText`, `border = '1px solid ' + colorBorder`. It sets onlyits accent: `colorAccent = var(purple)` (#c586c0). A token it uses must be in the import list,a new token must be added to tokens.hl (static) first.* hl:web names each token after its member (`colorTextMuted` → `--color-text-muted`), writes EVERYtoken of tokens.hl into the one `:root` (declaration order), then the app's `colorAccent` (a second`--color-accent`, later wins), and writes each use as `var(--…)`. Components/JS may still use`var(--color-…)` strings — the custom property names are the same.* hl:web does this itself since hybriel#39 (the webex LOCAL PATCH of antcolony mission 021 is gone, antcolony mission 036).* A change of tokens.hl or of styles.hl root members needs a RESTART: the dev watcher re-analysesbut the served sheet keeps the old values (measured, antcolony mission 021).* Deploy: `shared/` is part of the app folder; `deploy.sh`'s rsync sends it (proved in antcolony mission 021:deploy.sh excludes + debian:12-slim container → byte-identical `/__hl/app.css`).## Vendored Hybriel**hybriel master 06617221** (antcolony mission 074, 2026-10-03; adds the plugin allocators 3a781359 + 413f60e4 (#126:every plugin allocates with malloc via plugin_api.zig), mpackdb 2cb7ae5e (frees per-operation buffers), http1 773de63e (requestowns its parse), f0ac2d2d (event order; plugin ABI field — bin and .so must match); no lambda semantics change). sha256`21059cc741459ded8a004d44ed17c071aa296609be9fd0359202383210d77bdb`, built the same way from `git archive 06617221`(`~/scratch-074/src`, removed). Old vendor (190aa11d) in `.scratch/pre-074/`. Gates on 8760–8769: `TICKETS_GATE_PORT=8760TICKETS_GATE_IDENT_PORT=8761 TICKETS_GATE_EXCHANGE_PORT=8762 TICKETS_GATE_CHROME_A=8763-8764 TICKETS_GATE_CHROME_B=8767-8768TICKETS_GATE_CHROME_C=8769-8769 node tests/browser.mjs` → 249/0; connect.mjs as a temp copy (app URL :8762, Chrome 8763–8769,TICKETS_CONNECT_PORT=8760 TICKETS_CONNECT_IDENT_PORT=8761) → 60/0. Memory: `LONG=1 node .scratch/w074/mem.mjs <tag> <app tree>8760` (live-storage copy in `.scratch/w074/realdata/storage`, deleted after the run; prints RSS + swap): new 315 → 335 (200) →312 (1200) → 315 MiB (5700 pairs) — **flat**; old 190aa11d 319 → 358 → 451 → 743 MiB.Before: **hybriel master 190aa11d** (antcolony mission 072, 2026-10-02; adds fc838894 GC correctness (string index / plugin error messageread freed memory), 038d84b3 (#126 returned closure scopes collected), #127 (d98926c6, 04df4428); no lambda semantics change since7eea0d32). `bin/hybriel` sha256 `860f5e61878be75fe84a569eee03c95a25f2757940903b2ed0afee0871a23626`, built read-only from`git archive 190aa11d` (`~/scratch-072/src`, removed) with `/media/STORAGE/projects/hybriel/native/zig-toolchain/zig build-Doptimize=ReleaseFast -Dtarget=x86_64-linux-gnu.2.39` in `native/`; old copy (7eea0d32) in `.scratch/pre-072/`.Memory proof: `node .scratch/w072/mem.mjs <tag> <app tree> <port>` (needs a live-storage copy in `.scratch/w072/realdata/storage`,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.Before: **hybriel master 7eea0d32** (antcolony mission 069, 2026-10-02; ≥ f685f240 = #126 collector also by bytes, includes #48 #112 #117 #119 #120).`bin/hybriel` sha256 `f0d3019f46f83cbf119d0a426fab18394bfc6c02c58abaa725983fcc4bc734a0`, built read-only(`git -C /media/STORAGE/projects/hybriel archive master | tar -x -C ~/scratch-…/src`, then`cd native && /media/STORAGE/projects/termuplex/.tools/zig/zig build -Dtarget=x86_64-linux-gnu.2.39 -Doptimize=ReleaseFast`).`plugins/{core,crypto,data,fetch,fs,http,http1,mpackdb,proc,time,web}` = master's — **no local patch**.Re-vendor = copy the binary + these plugins, run the gate. The previous copy (73267707, sha 9707e0cc…) is in `.scratch/pre-069/`(the one before, 837fe120, in `.scratch/pre-048/`).**Lambda parameters COPY their argument since hybriel #48** (like methods); `&p` makes it a reference. A lambda that fills a list/recordit was given needs `&`: `migrate.hl` `migrateProjects/Tickets/Events/Members = (&log, …)` (without it the gate fails at"migration: at the first start the log says what was migrated"). Audit script `.scratch/w069/audit.py <files>` (param mutations +`for (x of param)` aliases). `store.hl` `sortByUpdated(rows)` sorts its copy and RETURNS it — callers use the return, fine.antcolony Mission 048: `migrate.hl`'s `compactNow()` block (hybriel #110) removed (proof logs `.scratch/w048/`). Lesson (hybriel #122):a post-face session sync must keep the route params — repro `.scratch/w048/repro/` (`node repro.mjs <bin>` must keep "you may edit").The old webex generation (e565176b + LOCALPATCHes #34 seed escape, #39 tokens) is backed up in `.scratch/pre-036/` (bin, plugins, sources, tests).* Framework pages use content-hashed URLs `/__hl/{hl-runtime.js,web/client.js,app.css}?v=…` (hashed =`immutable`, bare = `no-cache`). Check: `~/scratch-036/hashcheck.sh tickets.worldapi.org TICKETS /` (own server :8730).* Dropped with antcolony mission 036: plugins/webex + both LOCAL PATCHes, the NativeWebSocketServer listener(HL_HOST, #24), `sessions.resolve(cookie)` in /login/callback (→ `req.session`, #11), `realSession()`(→ `session == null`; hl:web refuses a forged trailing session itself, #16 — the gate's 12 forged checksaccept that ack `ok:false` "the `session` parameter is filled by the server"), the hand URL encoder(→ `encodeURIComponent`, #14), import.hl's charCodeAt string order (→ `a > b`, #2), jsoncheck's refusalof VALID surrogate pairs (#15; lone surrogates still abort JSON.parse → still 400).* Kept on purpose: `jsoncheck.hl` (JSON.parse still aborts on bad input), `localtime.hl` (no time zones),hand insertion sorts (no list `sort()`), `login.js` once-per-document guard (harmless).The binary finds `plugins/` beside the app (it walks up from the script's directory).`tests/cdp.mjs` + `tests/ports.mjs` are copies of the hybriel repo's `tests/browser/`.## Files| File | ||---|---|| `project.hl` | manifest: routes (pages + API function routes), audience || `store.hl` | the tables (UUID keys, `storage/mpackdb/`), rows, reads, writes, per-project numbers; relations + `links` table (#4) || `users.hl` | ticket #7: users + tokens tables, the ident exchange, sessions → users, the login env || `login.js` | the selector bridge (plain JS, served at `/login.js`) || `shared/md-editor.js` | ticket #12: `<md-editor>`, vendored copy of worldapi-components (served at `/md-editor.js`) || `components/you.hl` | `/you`: account + API tokens || `tests/identkit.mjs` | the gate's own ident (a copy without .env, mail sink) and its login helpers || `tools/migrate-008.hl` | one-off (2026-09-24, ticket #38): old global tables → `storage/mpackdb/`, idempotent || `tests/oldstore.hl` | the gate's old-format fixture for the migration || `localtime.hl` | epoch ms → Vienna wall time (userland DST rule) || `ticketfile.hl` | ticket file parser + `paragraphsOf` (shared by import and the fix tool) || `tools/fix-import-summaries.hl` | one-off (2026-09-24): re-paragraph stored summaries of imported tickets || `markdown.hl` | ticket #6: Markdown text → blocks/spans (safe links); `components/markdown.hl` renders them || `mdview.hl` | ticket #6: the `Accept: text/markdown` read view (documents for one ticket / a list); relations (#4) || `tests/markdown.hl` | prints markdown.hl's blocks for `MD_INPUT` (the gate's parser table) || `api.hl` | JSON replies, query/body helpers (`readBody` = JSON check + strict fields) || `jsoncheck.hl` | hand-written JSON syntax check (workaround for hybriel #12) || `import.hl` | the import command (needs `TICKETS_TOKEN`) || `styles.hl` | all CSS (imports the tokens from `shared/tokens.hl`, sets the accent) || `shared/tokens.hl` | the WorldAPI tokens (webex `var()`), verbatim copy of worldapi-tokens/tokens.hl — README "Design tokens" || `components/` | shell + three pages || `icons/` | antcolony mission 046: `icon.svg` (source), `icon-192/512.png`, `apple-touch-icon.png`, `favicon.ico` — README "PWA" || `tests/browser.mjs` | the gate || `tests/localtime.hl` | prints Vienna renderings of DST edges (the gate compares with `Intl`) || `docker-compose.yml`, `deploy.sh` | Byrodin container; the deploy from Loreana (section "Deploy") || `import/tickets/` | copies of the AntColony ticket files (source: byrodin) |## History and worker briefs- `LOG.md` — append-only history, one dated line per step (moved here from the antcolony LOG on 2026-10-01).- `missions/NNN-*.md` — worker briefs for this app; `reports/NNN-*.md` — their reports (same name). Numbered perproject since 2026-10-01 (antcolony#40); older text, code comments and commits use the old antcolony numbers →map: `/media/STORAGE/projects/antcolony-docs/docs/mission-map.md` (Byrodin: `/CONTAINERS/projects/antcolony/docs/mission-map.md`).
Branches
- mainmain branch
Latest commits
- 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