gitoriaLog in with ident

gitoria

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit205d5fe4205d5fe4gitoria: Hybriel master ff51cf46; ssh keys/tokens no double rows (session sync); gates follow #20mre205d5fe4/README.md

27.7 KB

  1. # gitoria.worldapi.org
  2. Git hosting for all projects, written in **Hybriel** (hl:web), login via **ident**. Source of truth: `CONCEPT.md`.
  3. Built so far: repos with their own address (#6), the repo homepage (#8), code browsing (#9), tickets (#10), pull requests (#11) with a Merge button for the owner (#17), releases (#12),
  4. every repo view as its own server-rendered page (#16), push and pull over HTTPS with access tokens and over SSH with keys (#7).
  5. ## Hybriel (vendored)
  6. * `bin/hybriel` + `plugins/` (core crypto data fetch fs http http1 mpackdb proc time web) = hybriel **master ff51cf46** (2026-10-01,
  7. mission 048: #113 mpackdb unique generated ids, #115/#116 lambda members on faces, #118, client SVG namespace, #111 hl:markdown
  8. (not vendored — gitoria has its own markdown.hl), session sync 0b17f65b/64527baa — see "Lesson" below; built the same way into
  9. `~/scratch-048-gitoria/src`, sha256 `f3b93a53…983588d3`; old copy `.scratch/pre-048/` = 317d4754 + the pre-048 tests/).
  10. Before: master 317d4754 (2026-09-26, mission 038; includes 7cb9f8fc = #107: an emit in flight when its socket closes is carried over, a page being left starts nothing
  11. — fixes the Firefox pull-back that rolled mission 037 back; also #103/#104, #105 `headers`, #106 `let` per loop pass, #94 files in
  12. emit, #82 reconnect; before: #83 hashed `/__hl/…?v=` URLs + immutable cache, #88–#91 Bytes / chunked bodies / base64 / run stdin,
  13. #95–#97, #100, #101, #45 hl:web, #80 `run()`, #81 `*path`, #44 `sessionDomain`), built from a read-only
  14. `git archive master` (never inside Anton's repo) into `~/scratch-038/src`: `/media/STORAGE/projects/hybriel/native/zig-toolchain/zig
  15. build -Doptimize=ReleaseFast -Dtarget=x86_64-linux-gnu.2.39` in `native/`, binary `native/zig-out/bin/hybriel`, sha256 `fc7481fb…48670a8`.
  16. Older copies: `.scratch/pre-038/` (13ef4f9b bin/ + plugins/ + the pre-038 `browser.mjs`), `.scratch/pre-037/`, `.scratch/pre-035/`, `.scratch/pre-033*/`.
  17. * **No local patch** (`grep -rn "LOCAL PATCH" plugins` finds nothing): a re-vendor is copy binary + plugins, run ALL THREE gates
  18. (browser.mjs, push.mjs, ssh.mjs — deploy.sh only runs browser.mjs).
  19. * **Lesson (048, hybriel 64527baa)**: a face that takes `session` answers with a `sync` of every component member derived from
  20. `session` through server code (`tokens`, `keyItems`, `me`, `codeData` …) — the member is ALREADY fresh when the handler goes
  21. on after `emit server`. A handler that adds the returned row must skip a row with the same id (tokens.hl, sshkeys.hl,
  22. tickets.hl `addTicket` do), or the row is listed twice.
  23. The session cookie's `Domain` is the manifest setting `sessionDomain` (project.hl).
  24. ## Run (dev, Loreana)
  25. ```bash
  26. cd /media/STORAGE/projects/gitoria.worldapi.org
  27. GITORIA_PORT=8360 GITORIA_PUBLIC_URL=http://localhost:8360 ./bin/hybriel project.hl
  28. ```
  29. Config (environment, or a `.env` beside `project.hl` — never printed or committed):
  30. | Variable | Default | |
  31. |---|---|---|
  32. | `GITORIA_PUBLIC_URL` | `https://gitoria.worldapi.org` | the main address; a repo lives at `<scheme>://<slug>.<host[:port] of this>` |
  33. | `GITORIA_PORT` | 8360 | |
  34. | `HL_HOST` | 0.0.0.0 | `127.0.0.1` on Byrodin behind nginx |
  35. | `GITORIA_WATCH` | on | `0` = no dev watcher (the container) |
  36. | `GITORIA_STORAGE` | `./storage/mpackdb` | table directory (`repos.db`, `users.db`) |
  37. | `GITORIA_GIT` | `<launch dir>/storage/git` | the bare git repositories, `<slug>.git` each (absolute path; the `git` binary must be installed) |
  38. | `GITORIA_TICKETS_URL` | `https://tickets.worldapi.org` | where a repo's tickets live (see "Tickets") |
  39. | `GITORIA_SESSIONS` | `.sessions/` | |
  40. | `GITORIA_COOKIE_DOMAIN` | `.<host of the public url>` | the session cookie's Domain (`-` = host-only; a host without a dot or an IP gets host-only) |
  41. | `IDENT_URL`, `IDENT_EXCHANGE_URL`, `IDENT_API_KEY`, `IDENT_API_SECRET` | as in tickets | login via ident |
  42. ## How a repo gets its address
  43. * A repo is one row in `storage/mpackdb/repos.db` (`@id` key, unique index on `slug`): `{ slug, description, owner, created }`.
  44. * **Slug**: unique in the whole system (one table, unique index); 2–40 characters `a–z 0–9 -`, starts and ends with a
  45. letter/digit, no `--`, not one of the reserved names (`repos.hl` `reserved`: technical host names only — www, api, git, mail, …; app names like ident or tickets are allowed).
  46. * **Address** = `<slug>.gitoria.worldapi.org`. nginx sends `gitoria.worldapi.org` and `*.gitoria.worldapi.org` to this one
  47. app (wildcard vhost, wildcard DNS and certificate: the architect's).
  48. * **Pages (gitoria#16)**: every page component declares `host = null` and hl:web hands it the request's host without port
  49. (hybriel#74) — on the server, on the first load and on every hl:web navigation. `users.hl slugOfHost` → '' (main address)
  50. or the slug. Routes (`project.hl`): `/` → `components/index.hl` (main address: `list.hl`, the repo list + "Create a repository";
  51. repo address: `readme.hl`), `/code` `/code/*` → `code.hl`, `/branch/*` → `branch.hl`, `/commit/*` → `commit.hl` (all three
  52. show `codebrowser.hl`), `/pulls` → `pulls.hl`, `/releases` → `releases.hl`, `/tickets` → `tickets.hl`, `/login/failed` →
  53. `loginfailed.hl`. Each repo page starts with `repohead.hl` (name, description, nav; unknown address → "No such repository").
  54. Links inside a repo are hl:web navigations (no page load). Rendered on the server with their content: the repo list,
  55. the repo head, the Readme's address/owner/created, the Tickets list (hl:fetch answers at once).
  56. * **Git on the server**: git is read with hl:proc `run()`, which waits for the program (hybriel#80), so the README, the file
  57. list / file, branches, commits, pulls and releases are in the first HTML and in every hl:web navigation (`git.hl`
  58. `homepageNow`, `browseNow`, `pullsNow`, `releasesNow`). No browser script is involved; `/host.js` is gone.
  59. * **The path of `/code/*path`, `/branch/*path`, `/commit/*path`**: hl:web binds the rest of the URL to the page member `path`
  60. (hybriel#81); the pages hand it to `codebrowser.hl`.
  61. * **Login**: ident's login *button* flow (`<ident>/login?key=&return=<main>/login/callback`), the code exchanged server side.
  62. * **Identity selector** (gitoria#14, as in tickets): ident's `<ident-selector>` sits beside the button in the header; choosing an identity
  63. hands its one-time code (`/login.js` → hidden `#identcode` → face `gitoriaLogin`) to the server for the same exchange — no reload, open
  64. tabs of the session follow. ident answers only a registered origin, so the selector shows on the main address only (the shell gives it
  65. the class `onrepo` from the request's host, `styles.hl` hides it); the button stays the way in there.
  66. The app is registered in ident with ONE origin, the main address. The session cookie carries `Domain=.gitoria.worldapi.org`
  67. (hl:web `sessionDomain`, hybriel#44), so the login holds on every repo address. A login started at
  68. `<slug>.…` returns through the main address and on to that repo (`?next=`, added by `/login.js` at the click: a path, or a full URL of `<valid-slug>.<host>`;
  69. `project.hl` `safeNext`). The first login asks for a display name (shown as the repo's owner). A failed login redirects to
  70. `/login/failed` (the reason parked in `session.data.loginError`).
  71. * **Short ids (ident#23, mission 039)**: `users.identity` holds what ident's exchange answers — since ident#23 the identity's
  72. public 5-character short id (`a68sz`), before that the per-app id (32 hex); `users.hl isIdentId` accepts both (the old
  73. `isHex` check refused short ids). The switch: one-off `tools/migrate-short-ids.hl` (old → short id, idempotent, never
  74. `finish`), gate `node tests/short-id-switch.mjs` (ports 8724/8725, no browser), runbook
  75. `antcolony-docs/docs/short-id-switch.md` (Byrodin: `/CONTAINERS/projects/antcolony/docs/short-id-switch.md` once synced).
  76. * **Create** (web form, face `gitoriaCreate`): any logged-in user with a display name. New repos reach every open list live.
  77. * **API** (public reads): `GET /api/repos`, `GET /api/repos/:slug`; `Accept: text/markdown` gives the Markdown read view.
  78. Creating is not in the API yet (needs API tokens — as in tickets `users.hl`).
  79. * Creating a repo also runs `git init --bare -b main` in `<GITORIA_GIT>/<slug>.git` (`git.hl`); pushing to it: see "Push and pull".
  80. ## Push and pull (gitoria#7)
  81. Git over **HTTPS**, answered by this app itself with the `git` binary (`transport.hl`; design: `docs/git-backend.md`). SSH: see below.
  82. * **Clone URL**: `https://<slug>.gitoria.worldapi.org/<slug>.git` (on the repo address, so the clone's folder is named like the repo). Paths
  83. `/<slug>.git/info/refs?service=…`, `/<slug>.git/git-upload-pack`, `/<slug>.git/git-receive-pack` (function routes, after the page routes in `project.hl`).
  84. * **Read** (clone, fetch, pull) needs no login: every repo is public. **Write** (push) needs the **access token of the repo's owner** as the
  85. password (any user name); anybody else's token → 403, no/unknown token → 401 with the way to get one. The token check is in `gitTransport`,
  86. before git is started.
  87. * **Access tokens** (`tokens.hl`, `components/tokens.hl`, section `#tokens` of the main address for a logged-in user): name → token `gtr_` + 40 hex, shown ONCE;
  88. only its sha256 is stored (`storage/mpackdb/tokens.db`); list, remove (works at once); at most 20 per user. Faces `gitoriaMakeToken`, `gitoriaRemoveToken`.
  89. * **The "Add code to this repository" box** (`components/repohead.hl`, gitoria#20): plain text (no toggle), only on the
  90. **Code page** of a repo that has no branch yet (`git.hl isEmptyNow`) — never on Readme / Pull requests / Releases /
  91. Tickets / Settings, and never once there is a commit. The clone command, the commands for a new project and for an
  92. existing one, and where the token comes from.
  93. * **How the body gets to git**: hl:proc `run()` has no stdin, so the request body is written to `<GITORIA_GIT>/.tmp/<random>.in` (a String holds raw bytes;
  94. hl:fs writes them exactly) and `sh -c 'git upload-pack|receive-pack --stateless-rpc "$1" < "$2" > "$3"'` (paths as arguments, no user text in the script)
  95. writes the answer to `.out`, which is read back as the response; both files are removed. A gzip request is unpacked first. `Git-Protocol: version=…`
  96. → `GIT_PROTOCOL` (v0, v1 and v2 tested). No hook: pulls and releases are read from the commits.
  97. * **Behind nginx** (the architect's vhost): `client_max_body_size` must allow pushes (say `500m`) and `proxy_request_buffering` stays **on** (default).
  98. git sends a big push chunked; hl:http1 reads chunked request bodies since hybriel#89 (mission 035: the old 411 hint is gone, a direct
  99. client without proxy pushes too — gate-checked). `proxy_read_timeout` ≥ 300s. The whole body is held in memory (≤ 500 MB).
  100. Kept on purpose (035): the temp files + `sh -c` (hl:proc `run()` could now take `stdin` + `binary`, hybriel#88/#91, but stdin
  101. crosses the plugin ABI as hex = twice the memory for a ≤ 500 MB body, and gzip would still need a second program) and `base64 -d`
  102. for the Basic header (hl:crypto `fromBase64(...).toString()` aborts the request on bytes that are not UTF-8).
  103. * Test: `node tests/push.mjs` (own ident + server + Chrome + the real git client; a small proxy in the gate plays nginx).
  104. Other ports: `GITORIA_GATE_PORT=8750 GITORIA_GATE_IDENT_PORT=8751 GITORIA_GATE_PROXY=8752 GITORIA_GATE_CHROME=8753-8757` (048: 46/0).
  105. ## Git over SSH (gitoria#7)
  106. A small **sshd container** (`docker/sshd`, service `gitoria-sshd` in `docker-compose.yml`) whose only job is `git-upload-pack` / `git-receive-pack`. It keeps no user list:
  107. * **Keys** (`sshkeys.hl`, `components/sshkeys.hl`, section `#sshkeys` of the main address for a logged-in user): paste a PUBLIC key + a name; checked with `ssh-keygen -l`
  108. (ed25519, ecdsa, sk-…, RSA ≥ 2048; a private key, DSA, junk, a second copy of a key are refused); list with fingerprint; remove; ≤ 20 per user. Table `storage/mpackdb/sshkeys.db`.
  109. Faces `gitoria{Add,Remove}Key`. A key works at once and stops at once (sshd asks again on every login).
  110. * **Login**: `AuthorizedKeysCommand` (`gitoria-keys`) → `GET /__git/keys?type&key` (`sshgate.hl`) → `restrict,command="gitoria-shell <user id>" <key>`. The forced command
  111. `gitoria-shell` accepts only `git-upload-pack|git-receive-pack '<slug>.git'` (slug validated, no shell, no forwarding, no tty), asks `GET /__git/access?user&slug&write`
  112. and only then runs git on `/repos/<slug>.git`. Same rule as HTTPS: **read = everyone, push = the repo's owner**.
  113. * **The two internal routes** exist only when `GITORIA_SSH_SECRET` is set; every call needs `X-Gitoria-Secret` = that secret, and a call that came through the public proxy
  114. (`X-Forwarded-For` / `X-Real-IP`) is refused. **SSH is offered on the site only when the secret is set** (the keys section and the ssh commands in the "add code" box are hidden otherwise).
  115. * **Deploy (the architect)**: `GITORIA_SSH_SECRET=<long random text>` (and optionally `GITORIA_SSH_PORT`, default 2222) in `.env` beside `docker-compose.yml` (both services read it);
  116. open the port in the firewall (port 22 belongs to the host's sshd, hence 2222: address `ssh://[email protected]:2222/<slug>.git`; with `GITORIA_SSH_PORT=22` the box shows `[email protected]:<slug>.git`);
  117. `gitoria.worldapi.org` (not only the wildcard) must resolve to Byrodin directly — **Cloudflare's proxy does not carry ssh** (use a grey-cloud/DNS-only record for the ssh host or a
  118. separate name; the address in the box uses the site's host name). The Hybriel image needs `openssh-client` (Dockerfile). `./storage/git` is mounted into the sshd container; its `git` account takes
  119. the uid of that folder's owner; host keys live in `storage/sshd-hostkeys/` (clients keep trusting the server). `docker compose up -d --build` builds both.
  120. * Test: `node tests/ssh.mjs` (048: `… GITORIA_GATE_SSH_PORT=8758` + the push ports → 44/0) (builds and starts the REAL sshd container on port 8708 with host networking; real ssh + git: clone, push 3 MB, RSA + ed25519 keys, pull, unknown key, no shell,
  121. path tricks, no forwarding, another user may read not push, removed key stops at once). Needs docker.
  122. ## The repo homepage (`/` of a repo address)
  123. * The repo's **README.md** (root, any case) is shown as a page; if the repo has a **`$docs`** folder, **all Markdown files inside it**
  124. (subfolders too, in path order, at most 30) form the homepage instead and the root README is not shown. Read from the
  125. repo's `HEAD` (the branch setting comes with #9). No commit / no README → "This repository has no README.md yet."
  126. * Reading = the `git` binary via hl:proc (`git.hl`: `ls-tree -r`, `cat-file blob HEAD:<path>`, argv list, paths only from git,
  127. 15 s limit, ≤ 5000 lines a file),
  128. read while the page is built on the server (`homepageNow`).
  129. * Markdown → HTML (`markdown.hl` copied from tickets, plus tables, block quotes, rules; `components/markdown.hl`): built as
  130. elements from parsed data, never an HTML string — raw HTML in a README is shown as text, only http(s)/mailto/`/…`/`#…` links
  131. are links. Not rendered: images, nested lists (shown as typed).
  132. ## Browsing code (`/code`, `/branch/<name>`, `/commit/<id>` of a repo address)
  133. * **`/code`** = the repo's main branch at its last commit. Main branch = the owner's setting (repo field `branch`), else `main`,
  134. else the first branch. The owner sets it on the code page: "Make main" beside each other branch (only the owner sees it; the
  135. face checks the owner again). The homepage (README / `$docs`) reads the same main branch.
  136. * **`/branch/<name>`** = that branch at its last commit (a name with slashes works: the longest existing branch name wins).
  137. **`/commit/<id>`** = the whole project at that commit (`<id>` = 4–40 hex characters, resolved to the full id).
  138. * After each of them a path: `/code/src/a.txt`, `/branch/feature/x/src`, `/commit/<id>/src`. A folder shows its entries
  139. (folders first, then files, with size), a file its numbered lines (at most 2000 lines, at most 1 MB). Also on the page: the
  140. crumbs, the latest commit, all branches, the latest 20 commits (each links to `/commit/<id>`).
  141. * Read with the `git` binary (`git.hl` `browse`: `for-each-ref`, `log`, `cat-file`, `ls-tree`, argv lists, `--literal-pathspecs`).
  142. Read on the server while the page is built (`browseNow`). `/code`, `/branch/*`, `/commit/*` are three pages sharing
  143. `codebrowser.hl`; a link inside the app is an hl:web navigation. "Make main" (face `gitoriaSetBranch`) answers with the view again.
  144. * **Only UTF-8 text is shown**: a binary file or one that is not valid UTF-8 says so instead (its bytes would break the
  145. page's socket). Paths with `:`, quotes, backslashes or control characters are not browsable (`git.hl` `safePath`).
  146. * Not built: the Markdown read view / API of code, a diff of a commit, images, syntax highlighting, downloading a tree.
  147. ## Tickets (`/tickets` of a repo address)
  148. * The tickets are **not stored in gitoria**: they live in tickets.worldapi.org, in a project named `<slug>.<host of GITORIA_PUBLIC_URL>`
  149. (e.g. `myrepo.gitoria.worldapi.org`; a repo slug has no dot, so it never meets another repo's project or the dotted app projects).
  150. Anyone logged in to tickets sees them like any tickets project. `tickets.hl` talks to tickets' public API.
  151. * **List**: `GET <tickets>/api/projects/<project>/tickets` (public), newest update first, at most 200 shown: number, subject (as text), state,
  152. last update; each links to the ticket in tickets (read, comment and change the state there — the list view only is in gitoria).
  153. No project yet (404) → "No ticket yet". Tickets down → "tickets.worldapi.org did not answer". Read when the page is built on
  154. the server (hl:fetch is synchronous): the list is in the first HTML. A ticket opened here shows up live on every open tickets page of that repo.
  155. * **Connect (gitoria#18)**: the repo's **settings** (`/settings`, owner only) has "Tickets: connect". It sends the owner to
  156. `<tickets>/connect?app=gitoria&label=<slug>&return=<repo address>/settings/connected&state=<nonce>` (tickets asks which project they are
  157. admin of); tickets returns `?code&state`; gitoria checks the nonce (parked in the session, bound to the repo), exchanges the code from
  158. the server (`POST /api/connect/exchange`) and stores the key per repo in `repos.db` (`tktKey`, never sent to a page). "Forget the
  159. connection" clears it locally (tickets can also disconnect). Not connected → `/tickets` says so and points to Settings.
  160. `GITORIA_TICKETS_TOKEN` and the auto-created `<slug>.<host>` project are gone (they were the old way).
  161. * **Open a ticket**: any logged-in user with a display name: `POST <api>/tickets` with `Authorization: Bearer <key>` and
  162. `X-Tickets-Identity: <the user's ident id>` — the person is the author in tickets, under the project's roles (they must have logged
  163. in to tickets once).
  164. * **`#N` links both ways**: `#12` in a commit subject (code page, latest commits) or a pull request title links ticket 12. A push
  165. (and the Merge button) runs `notifyPush`: every commit of the last 100 on any branch whose subject names `#N` posts a comment
  166. "Mentioned in commit …" on ticket N (once per commit and ticket, remembered in `ticketlinks.db`), and a **merged** `|||PR` whose
  167. title says `fixes|closes|resolves #N` sets ticket N to state `review` with a comment. Done as the repo's owner. Commits that
  168. exist when the repo is connected are only marked, not announced.
  169. * Not built: showing a ticket's text / comments inside gitoria, editing a ticket from gitoria, a state filter, API of gitoria for tickets.
  170. ## Pull requests (`/pulls` of a repo address)
  171. * Nothing is stored: the list is read from git each time (`git.hl` `pulls`). A **pull request is a commit** whose subject starts
  172. `|||PR ` (target = the repo's main branch, i.e. the owner's setting, else `main`) or `|||PR|<branch>] ` (target = `<branch>`).
  173. Anything else (marker not at the start, no space after `]`, an invalid branch name) is a normal commit.
  174. * Title = the text after the marker (empty → the source branch's name). Source = the branch that holds the commit and is not the target
  175. (`for-each-ref --contains`); one request per source branch (its newest marker commit); 50 at most, from the latest 500 commits of all branches.
  176. * State: **merged** when the commit is in the target branch (`merge-base --is-ancestor`), else **open**; a target that does not exist is
  177. shown as "(no such branch)". Merging is done with git itself (push to the target) — no merge button yet.
  178. * Read on the server while the page is built (`pullsNow`).
  179. ## Releases (`/releases` of a repo address)
  180. * Nothing is stored: read from git each time (`git.hl` `releases`). A **release is a commit on the repo's main branch** whose subject starts
  181. `|||RL ` (patch +1), `|||RL|med ` (minor +1, patch 0), `|||RL|mj ` (major +1, minor and patch 0) or `|||RL|<version> ` (set by hand,
  182. `1.1.1a`: three numbers, then letters/digits/`.`/`-`). `|||RL` alone counts as `|||RL `. Anything else is a normal commit.
  183. * Versions are counted from 0.0.0 over the main branch's history, oldest to newest (first `|||RL ` = 0.0.1); after a hand-set version the count
  184. goes on from its numbers. A hand-set version already released is not a release. Shown newest first (200 at most, latest marked): version, text
  185. after the marker (else the short commit id; links to `/commit/<id>`), author, date. Read on the server while the page is built (`releasesNow`).
  186. * A release is only that: version + commit. No git tag is written, no archive to download (the concept does not say what else it holds).
  187. ## PWA (installable app, mission 046)
  188. The installable app, the same way calendar.worldapi.org and tracker do it: hl:web's own manifest and service worker from
  189. settings in `project.hl`, no JavaScript of ours.
  190. * `appIcons` (192 + 512 PNG, each `any` and `maskable`), `appTouchIcon` (180 PNG), `appFavicon` (`/icons/favicon.svg`),
  191. `appThemeColor` = token `darker` (the header, rgb(15, 20, 25)), `appBackgroundColor` = token `dark` (rgb(25, 30, 35));
  192. name = `appTitle` "gitoria". hl:web serves `/__hl/manifest.webmanifest` (start_url/scope `/`, display standalone) and
  193. `/__hl/sw.js`, and links manifest, apple-touch-icon and theme-color from every head. `/favicon.ico` is a real icon
  194. (16/32/48). Each icon has its own `file` route in `project.hl`. Every repo address is its own origin: it gets the same
  195. manifest and its own worker (installed from a repo address, the app opens that repo's Readme).
  196. * **Icons** (`icons/`): `icon.svg` is the source (512, hand-written: a git branch — trunk with two commits, a branch
  197. curving off to a third; `#569bd4` on rgb(25,30,35); everything inside the maskable safe zone, a circle of radius 204, so
  198. one image serves `any` and `maskable`); `favicon.svg` is the same drawing, thicker, cropped tight on a rounded tile.
  199. Rendered on Loreana:
  200. ```bash
  201. rsvg-convert -w 192 -h 192 icons/icon.svg -o icons/icon-192.png
  202. rsvg-convert -w 512 -h 512 icons/icon.svg -o icons/icon-512.png
  203. rsvg-convert -w 180 -h 180 icons/icon.svg -o icons/apple-touch-icon.png
  204. for s in 16 32 48; do rsvg-convert -w $s -h $s icons/favicon.svg -o /tmp/fav-$s.png; done
  205. magick /tmp/fav-16.png /tmp/fav-32.png /tmp/fav-48.png icons/favicon.ico
  206. ```
  207. * **Offline**: `offline = [ Index ]` — the worker precaches the shell (runtime, modules, CSS, manifest, icons) and the
  208. document of `/`. Navigations are network-first (an online visit of `/` refreshes the kept copy); without a network `/`
  209. comes from the cache AS LAST SEEN (main address: the repo list; repo address: its Readme) and every other page (code,
  210. branch, commit, pulls, releases, tickets, settings) gets hl:web's "Unavailable offline" page (503). The shell
  211. (`components/main.hl`) shows "You are offline. Repositories and code need the network." while `navigator.onLine` is
  212. false: hl:web gives a page no connection state and no mount hook, so an invisible `netProbe` runs an endless 1 s CSS
  213. animation (`styles.hl` `@keyframes gitoria-net-tick`) whose `animationiteration` handler reads `navigator.onLine`.
  214. A server that is down while the device is online shows no note.
  215. ## Test
  216. `node tests/browser.mjs` (199 checks; commits real files into the gate's bare repos) — own gitoria + own ident + own tickets (copies without `.env`, mail sink; `tests/ticketskit.mjs`) + headless Chromes; `*.gitoria.test`
  217. is mapped to 127.0.0.1 inside Chrome (`HL_CHROME_ARGS`, `tests/cdp.mjs`). Ports 8700–8709 (gitoria 8700, ident 8701, tickets 8702, Chromes 8703–8709); another range:
  218. `GITORIA_GATE_PORT=8710 GITORIA_GATE_IDENT_PORT=8711 GITORIA_GATE_TICKETS_PORT=8712 GITORIA_GATE_CHROME=8713-8719 node tests/browser.mjs`. Screenshots in `.scratch/gate-*.png`, server log `.scratch/gate-server.log`.
  219. Last run (mission 048): the same with ports 8750–8758 → `199 passed, 0 failed`. Before (mission 046): `GITORIA_GATE_PORT=8720 GITORIA_GATE_IDENT_PORT=8721 GITORIA_GATE_TICKETS_PORT=8722 GITORIA_GATE_CHROME=8723-8727 GITORIA_GATE_FIREFOX=8728 node tests/browser.mjs` → `199 passed, 0 failed` (the Firefox port defaults to 8699 — set it inside your range).
  220. * PWA block (mission 046, at the END of the gate): manifest + icons over HTTP (real PNG sizes, favicon blue, `/favicon.ico` an
  221. ICO), then the gate's Chromes are CLOSED and one fresh Chrome gets `--unsafely-treat-insecure-origin-as-secure=<main>,<alpha>`
  222. (a service worker needs a secure context; plain-http `*.gitoria.test` is not one — the live https site needs nothing):
  223. Chrome's installability + manifest verdict, worker scope `/` on the main AND a repo address; offline: the tab's network
  224. is cut (the note appears live), the SERVER is stopped (CDP offline does not reach the worker's own fetches), reload of `/`
  225. shows header + note + the list as last seen, alpha's `/` its Readme, `/code` "Unavailable offline"; server restarted,
  226. back online. Screenshots `.scratch/gate-pwa-phone-{online,offline}.png` (390 px).
  227. * gitoria#16 block: `firstHtml(path, host)` fetches the FIRST HTML with a Host header (node's fetch cannot set one) — `/` (README), /code,
  228. /code/src, a file, /branch/main, /branch/feature/x, /commit/<id>, /pulls, /releases, /tickets hold their real content and no "Loading";
  229. the main address the repo list; hybriel#43: a code view in a second tab of the session keeps its elements through a login and a
  230. logout of the other tab; every view of an unknown address "No such repository"; `/host.js` 404; in Chrome every view loads
  231. directly, and Readme → Code → Releases → Tickets → Pulls → Readme plus a folder + Back keep `window.__navMarker` (no page load).
  232. * Re-vendor block: `/__hl/app.css` has the token file's `--dark` in `:root` (hybriel#39); a repo description
  233. `</script><b id="xss">…` stays inside the seed (`\u003c`) and shows as text (hybriel#34); `Domain=.gitoria.test` on the cookie (`sessionDomain`).
  234. * Navigation LOGGED IN (the creator saw full page loads in Firefox): a fresh Chrome logs in with the button on a repo address, then
  235. real clicks Readme → Code → Releases → Pulls → Tickets → Code → a folder keep `window.__navMarker`; the same on an empty repo the user
  236. owns, and with the WebSocket closed (POST fallback). The same two runs in a **real Firefox** (`tests/firefox.mjs`: headless
  237. `/usr/bin/firefox` over WebDriver BiDi, no driver/npm; hosts mapped with the pref `network.dns.localDomains`; BiDi port
  238. `GITORIA_GATE_FIREFOX`, default 8699).
  239. * By hand: `curl -s -H 'Host: <slug>.gitoria.test:8720' http://127.0.0.1:8720/code` against a running dev server.
  240. * Lesson: the views are in the FIRST HTML now, so "content is there" no longer means "page is live" — `await hydrated(page)` before
  241. clicking a button with a handler ("Make main" was clicked on the dead SSR page and flaked).
  242. ## Deploy
  243. `./deploy.sh` (gates → backup → rsync → restart → 200). First deploy = the architect's: folder, `.env`, wildcard vhost
  244. (`server_name gitoria.worldapi.org *.gitoria.worldapi.org;`, WebSocket upgrade headers), wildcard DNS + certificate, and the app
  245. registered in ident with origin `https://gitoria.worldapi.org`. Container port 45004 (`docker-compose.yml`).

Branches

Latest commits

  • 205d5fe4gitoria: Hybriel master ff51cf46; ssh keys/tokens no double rows (session sync); gates follow #20mre
  • 9b27cb26gitoria#21: installable app (manifest, service worker, offline start page), own iconmre
  • 68dcb603deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • e2deed6dgitoria#20: "Add code" only on the Code page of an empty repository, no collapsiblemre
  • 8bb97ffddeploy.sh: never send .git or .gitignore to Byrodinmre
  • fd981932State of 2026-09-27; bin/ no longer tracked (Hybriel commit is in README)mre
  • 4a2d7125initial commitmre