lmjtfy.git / apps / lmjtfy / README.md
README.mdpreviewREADME.mdsource664 lines · 36.8 KB · raw
1# Chapter 2: the Worker, where everything actually happens
2
3Everything you see at <https://lmjtfy.fun> comes out of this
4folder. It is one Cloudflare Worker: a small program Cloudflare runs at its
5edge, started fresh for each request in milliseconds, written here in Rust
6and compiled to WebAssembly. There is no server to keep running. A request
7arrives, the Worker answers it, and that is that.
8
9Two things have to outlive a single request: what was already asked (so it
10is never asked again) and how much of today's budget is left. For those the
11Worker has two Durable Objects, which are the closest Cloudflare comes to a
12tiny server with a memory: one object per name, anywhere in the world, with
13its own SQLite. The Worker talks to them; they remember.
14
15```mermaid
16flowchart LR
17  V["Visitor's browser"] -->|"/, /ask, /gate"| W["Worker (src/lib.rs)"]
18  V -->|"/live (socket)"| A
19  G["git clone"] -->|"/lmjtfy.git"| W
20  W --> A["Archive object: every call, the feed, the sockets"]
21  W --> B["Budget object: today's spend, per-visitor limits"]
22  A --> J["Jev (TypeSafe)"]
23  A --> L["LLM (Workers AI)"]
24  W --> GH["GitHub (read-only token)"]
25```
26
27## What it serves
28
29| Address | What happens |
30| --- | --- |
31| `/` and `/?q=...` | The page. With `?q=`, it types the question in for you and asks. |
32| `POST /ask` | Answers a question as a stream: the whole transcript, re-sent as each call finishes. |
33| `POST /rate` | A browser's 👍 or 👎 on Jev's answer, and the votes after it. |
34| `POST /comment` | The words a browser adds to its vote, from the box that opens under the 👍 and 👎. Trimmed, 1,000 characters at most, five a minute a visitor, and only while the browser has a vote; answered with "Thanks" or why not, in fixed words. The text is kept (below) and is in no log line or event. |
35| `POST /seen` | A page's report of itself as it is left: how long it was in view, how far down, the screen; or a link followed off the site. Kept, and answered with nothing. |
36| `POST /gate` | Run as you type, 300 ms after each pause. First, the questions already on the feed that what is typed could be the start of, which asks nobody anything; then the facts request, so the answer starts from what Jev already said. Text that stops part way is said to be unfinished, and not judged. |
37| `/feed` | The next page of "asked lately", which the feed asks for when it is scrolled to its end. It reads the archive and asks nobody anything. |
38| `/live` | A WebSocket each open page holds: how many are online, toasts, live feeds, and "a new version is live". |
39| `/rules` | The rules engine with every fact clickable, asking nobody anything. |
40| `/robots.txt` | Asks crawlers to leave `/rules?…` alone: with facts set it is a page per combination, each linking to more. |
41| `/rules.svg` | The rules drawn as a standalone picture, for the docs (chapter 6). |
42| `/card.png` | The picture a link unfurls with (chapter 11). |
43| `/icons/<name>.png` | The explorer's file and folder icons (chapter 15⅝). |
44| `/lmjtfy.git` and the other repositories | With `GIT_REDIRECT` on, redirected with the same path and query to `code.lmjtfy.fun`; with it off, served here as before (below). |
45
46And at `code.lmjtfy.fun`, which is the same Worker answering to another name and serving only this:
47
48| Address | What happens |
49| --- | --- |
50| `/` | Every repository served, with its `git clone` command and a copy button. |
51| `/lmjtfy.git` | `git clone` it, or open it in a browser and read the code (you are probably here). |
52| `/jevcrates.git`, `/postjevsql.git`, `/jevsnes.git`, `/jevhooks.git`, `/jevstrudel.git`, `/whiskers.git` | The client lmjtfy shares with the owner's other Jev projects, and those projects, served the same way. |
53| `/whiskers/latest/<kind>` | The file of that repository's latest GitHub release of that kind (below), downloaded through the Worker. `GET` and `HEAD`, `Range` honoured. Cached five minutes. |
54| `/whiskers/releases/<tag>/<asset>` | That exact file of that exact release. Cached for good. |
55| `/whiskers/latest/`, `/whiskers/releases/` | A page listing the latest release, or the recent ones, each file with its size and a link. |
56| `/icons/<name>.png`, `/emoji.woff2`, `/card.png?code=`, `/robots.txt` | What those pages draw themselves with. |
57
58> **Try it.** Open <https://lmjtfy.fun/rules> and click
59> `answerable` until it says no. Watch every rule but one go grey. That is
60> the engine from chapter 6 running in your browser's address bar.
61
62## Live: who is here, and what just happened
63
64Every browser with the site open keeps one socket to `/live`, held by the
65archive object. Its sockets are "hibernatable": while nothing happens, the
66object can sleep and nothing is billed, and the sockets stay open.
67
68One socket per browser, not per tab: the socket belongs to a SharedWorker
69(`src/live.js`), a script the browser runs once for all of a site's tabs
70and keeps while any of them is open. Each tab talks to it, and it passes on
71everything the archive sends; a tab that opens later is handed the current
72count and places at once. A tab the browser freezes in the background
73cannot take the socket down with it, which is what goes wrong when one tab
74holds the socket for the rest. A browser without SharedWorker opens a socket
75per tab, as every page used to.
76
77> **Aside.** Pages of different builds use different workers (the build is
78> in the worker's address), so after a deploy a reloaded tab gets a fresh
79> one while a tab from before keeps its own until it reloads too.
80
81It pushes four things:
82
83- **How many browsers are open, and where**, shown as "N online" in the top
84  bar; hover it for a flag and a place for each, most first. The place is
85  Cloudflare's: every request arrives with a rough country and city, and the
86  Worker passes them to the archive when a page connects (in headers it sets
87  over any the page sent, so a page cannot name its own). The archive keeps
88  them on that page's socket while it is open, for the list. (What is kept
89  for good is below, in [What is kept about visitors](#what-is-kept-about-visitors).) Pages
90  are told at most once a second, so a deploy, which reconnects every page
91  at once, is one update rather than one per page. A page that reconnects
92  while the deploy is still reaching Cloudflare's edge can come through the
93  Worker from before, which passes no place; ten seconds on, when the deploy
94  has settled, the archive asks it to reconnect (close code 1012), and it
95  comes back placed.
96- **A toast when someone asks a question or clones the code.** A question
97  the feed may show is shown with Jev's answer; any other is "someone asked
98  Jev something". The page that asked does not get its own toast. The site
99  sends pages nothing else about who is connected: no address, no browser,
100  no history.
101- **The feeds themselves.** After each toast the archive sends the home
102  page's feeds as HTML elements with ids, and a page replaces the ones it
103  has: most asked and so far whole. A question just asked goes to the top
104  of asked lately, taking its line from wherever it was, so the older lines
105  a page has scrolled in stay put. No refresh.
106- **The build.** Every page carries the commit its Worker was built from
107  (`build.rs`). A deploy restarts the archive object, every socket closes,
108  every page reconnects, and the archive tells each one the build that is
109  live now. A page from an older build shows a toast that stays, with a
110  Reload button. If the deployed commit has a `Release-Note:` trailer, an
111  older page also gets that line as a toast, so the people on the site hear
112  what changed.
113
114## The code pages
115
116    git clone --recurse-submodules https://code.lmjtfy.fun/lmjtfy.git
117
118The address is `code.lmjtfy.fun`. With `GIT_REDIRECT` on, the old
119`https://lmjtfy.fun/lmjtfy.git` (and `www`, and `workers.dev`, and every
120other served repository) is sent there with a `308`, path and query kept: git follows the redirect of its
121first request, `GET /lmjtfy.git/info/refs?service=git-upload-pack`, and
122makes every request after it at the new address, so `git clone
123https://lmjtfy.fun/lmjtfy.git` still works, and so does the relative
124submodule URL `../jevcrates.git`, which git resolves against the address the
125redirect gave it. A `POST` that reaches the old address is not git
126following a redirect, so it is not redirected: it is answered `405` with the
127address to use. A `GET` is a `308` because a client that must keep its method
128should; a person's browser follows either.
129
130### The switch, `GIT_REDIRECT`
131
132The redirect is a Worker var, `GIT_REDIRECT`, `"on"` or `"off"`
133(`[vars]` in `wrangler.toml`, which says `"off"`), so the code host can be
134deployed and checked before anyone is sent to it. It is read by
135`host::GitRedirect`, which accepts exactly those two words and an unset
136var as off. Anything else (`ON`, `true`, an empty string) is logged as an
137error on every request and runs as off: off is the behaviour that cannot
138break a working clone, where on would send every clone to an address that
139may not be live.
140
141| | `off` (default) | `on` |
142| --- | --- | --- |
143| `lmjtfy.fun/<repo>.git…` | Served here, as before the code host | `308` to `code.lmjtfy.fun`, path and query kept; a `POST` is answered `405` |
144| `www` and `workers.dev`, a repository's path | `301` to `lmjtfy.fun`, which serves it (git follows it) | `308` straight to the code host |
145| `www` and `workers.dev`, anything else | `301` to `lmjtfy.fun` | the same |
146| `code.lmjtfy.fun` | Serves the repositories and the front page | the same |
147| The home page's clone command and "Get the code" link | `https://lmjtfy.fun/lmjtfy.git`: no page names the code host | `https://code.lmjtfy.fun/lmjtfy.git` |
148| Code pages on the apex: the clone command | The apex | The code host (and the apex's paths never get that far: they redirect) |
149| `lmjtfy.fun/<name>/latest…`, `/<name>/releases…` (release files) | `404`: not served here, in either mode | `308` to `code.lmjtfy.fun` like a clone (a `POST` is `405`) |
150| Staging and dev servers | Serve it themselves, release files too | the same: no code host there |
151
152The links follow the switch so that no page advertises an address that has
153not been checked. The code host answers in both modes.
154
155#### Rolling out
156
157The order matters: **the code host is deployed and checked before the apex
158sends anyone to it.**
159
1601. Deploy with the var off (the toml's default). This ships the code host,
161   its front page and the custom domain; `lmjtfy.fun/<repo>.git` serves
162   clones exactly as before, and the home page still names the apex. Check
163   by hand, since `tools/check-hosts` expects the token to be missing:
164   `curl -sI https://code.lmjtfy.fun/` is `200`, and
165   `git clone --recurse-submodules https://code.lmjtfy.fun/lmjtfy.git`
166   works, submodules included. The custom domain's certificate can take a
167   few minutes.
1682. Flip it: `lmjtfy-wrangler apps/lmjtfy deploy --var GIT_REDIRECT:on`, or
169   change the toml to `"on"` and deploy. Check
170   `curl -sI https://lmjtfy.fun/lmjtfy.git/info/refs?service=git-upload-pack`
171   is a `308` to the code host, and `git clone --recurse-submodules
172   https://lmjtfy.fun/lmjtfy.git` (through the old address) still works.
1733. To go back: `lmjtfy-wrangler apps/lmjtfy rollback` (the previous
174   version, which has no redirect), or deploy with
175   `--var GIT_REDIRECT:off`. Make the toml say what is live afterwards.
176
177The Worker gives each host its own routes (`src/host.rs`). The code host has
178the repositories, their pages and the front page, and nothing that asks Jev
179or reads the archive: no `/ask`, `/feed`, `/live` or `/seen`, so its pages
180show no online count and no toasts. Staging has no code host (a second
181address is a second way round Access); it serves the repositories itself,
182where it is.
183
184## Release files: the downloads of a private repository
185
186`src/release.rs` serves the files of a repository's GitHub releases, so a
187release of a private repository can be downloaded by anyone (the whiskers
188site links to these addresses). `<name>` is a served repository's name
189without `.git`.
190
191| Address | What |
192| --- | --- |
193| `/<name>/latest/<kind>` | The file of the latest release whose **kind** is `<kind>`. `Cache-Control: public, max-age=300`, because it moves. |
194| `/<name>/releases/<tag>/<asset>` | The exact file of the exact tag. `public, max-age=86400, immutable`. |
195| `/<name>/latest/`, `/<name>/releases/` | A page (the code pages' look) of the latest release, or the last twenty, with sizes and links. |
196
197Anything else under `/<name>/latest` or `/<name>/releases` is a plain `404`,
198and so is a name that is not in `Repo::ALL`. A path segment may hold only
199letters, digits and `. _ + ~ @ -` (never `.` or `..`), so there is no
200percent-encoding, encoded slash or dot-dot to be decoded later.
201
202**A file's kind is its name with the release's version taken out**: everything
203after the first `-<tag>-` in the name (`whiskers-2026.10.5-arm64.apk` is
204`arm64.apk`; `whiskersd-2026.10.5-x86_64-linux.tar.gz` is
205`x86_64-linux.tar.gz`). A tag with a leading `v` is also looked for without
206it. A name with no `-<tag>-` in it, such as `SHA256SUMS`, is its own kind. So
207the address of "the arm64 build" does not change when the version does. If
208two files of the release have one kind, that kind is a `404` and the log
209names the release: it is never a guess. The pinned address is always exact.
210
211How a file is served: the release is looked up through the GitHub API with
212the same read-only token (`/releases/latest`, `/releases/tags/<tag>`; kept a
213minute per isolate, good answers only). The file is asked for at
214`/releases/assets/<id>` with `Accept: application/octet-stream`; GitHub
215answers `302` to a short-lived signed address, which is followed by hand and
216fetched **without** the token, and its body is streamed to the visitor, never
217held in memory. A file is answered as the runtime's own response (`Served::File`,
218before the router): an `http` body is re-written through Rust as a plain
219stream, which is sent chunked with no length, and a download bar needs the
220length. The content length passes through; the type is chosen from
221the name (`.apk` is `application/vnd.android.package-archive`, `.tar.gz` is
222`application/gzip`, `SHA256SUMS` is `text/plain`, else
223`application/octet-stream`), with `content-disposition: attachment` and
224`x-content-type-options: nosniff`. `HEAD` answers from the release's own
225record of the size and fetches nothing. A `Range` header of plain
226`bytes=…` is forwarded to the signed address, which answers `206`. GitHub
227failing is a `502` in fixed words; no GitHub text is in a response or a log.
228
229A download from its start (no `Range`, or one from byte 0) is an event of
230`what` `download` in the archive, its detail `<name>/<file name>`. `what` is
231free text, so no migration was needed. The GET is not also a `view`
232(`event::is_download`), and nothing shows a download on the home page: no
233toast, no count. The two listing pages are views.
234
235Only the code host serves these (`Host::serves_downloads`), and a dev server
236or staging, which have no code host.
237
238To add a repository: add its name to the
239`repositories!` list in `src/clone.rs` and answer the compiler's `match`
240errors there. That one list is what is served, what the code host's front
241page lists, and what the old addresses redirect; a test holds the three
242together. Give the GitHub token Contents: read on it too.
243
244The repository is private on GitHub, and is shared from here instead, so
245nobody needs a GitHub account and nothing is announced. The Worker speaks
246git's smart HTTP (`src/clone.rs`): the two requests a clone or fetch makes,
247`info/refs?service=git-upload-pack` and `git-upload-pack`, are forwarded to
248GitHub with a fine-grained token that can read the served repositories and nothing else. A push is refused here, and GitHub
249would refuse it anyway. jevcrates is served beside it at `/jevcrates.git`,
250because `.gitmodules` names it by the relative URL `../jevcrates.git`, which
251git resolves against wherever lmjtfy was cloned from. The owner's three other
252Jev projects (postjevsql, jevsnes, jevhooks) are served too, as is whiskers (which uses Jev as its guard), and name
253jevcrates the same way, so each clones with its submodule from here.
254
255Clones and pulls are counted, anonymously, per repository; "So far" shows
256lmjtfy's, and a clone of any project but jevcrates is toasted. jevcrates is
257fetched along with every project cloned with its submodules, so its toasts
258would double up. A pull names the commits it
259already has (`have` lines) and a clone does not; a fetch counts on the round
260GitHub answers with the pack, so a long negotiation counts once and a pull
261with nothing new is not counted.
262
263Git only ever asks for `lmjtfy.git/info/refs` and `lmjtfy.git/git-upload-pack`,
264so every other path under `/lmjtfy.git` is free for people. The code pages
265(`src/browse.rs`) are laid out like an editor, full width. On the left, a
266sidebar of panels: the explorer (the whole tree, jevcrates included,
267opened on the way to the page you are on), the outline of the page's
268headings, the clone command, the latest commit, and every repository served
269here; jevcrates' pages add the `Cargo.toml` lines for depending on it, pinned
270to its latest commit. On the right, the page:
271a folder's README and CLAUDE.md as two tabs, "for people" and "for agents",
272between a ◀ tab for the chapter before and a ▶ tab for the chapter after
273(from the chapter's own last line, or, in a repository with no guide, the
274next folder with a README in a depth-first walk),
275with their diagrams drawn (click one to enlarge it); a source file
276with its comments rendered on the left and the code they are about on the
277right, coloured (chapter 12½), or rendered if it is markdown, each with a tab
278for its source top to bottom; `?raw`
279gives a file as it is. `/lmjtfy.git` itself is the root folder, whose
280README is the prologue. On a phone the sidebar moves below the page.
281
282Everything is read from GitHub's API with the same token, kept a minute per
283isolate: folders and files from the contents API, the explorer's tree in one
284request from the git trees API. What each repository says it is (the
285description, homepage and topics on the code host's front page, in the
286sidebar, the page's link preview and the picture) is GitHub's own, from
287`GET /repos/{owner}/{name}`, asked for all the repositories together and kept
288the same minute. Nothing about a repository is written here: change it on
289GitHub. If GitHub cannot be read, or a repository has no description, its card
290simply has none. jevcrates is browsed under
291`third-party/jevcrates/` at the commit lmjtfy pins.
292
293## Storage
294
295Two Durable Objects keep everything that outlasts a request. Each is one
296object for the whole site (`id_from_name`), so every visitor reads the same
297rows.
298
299### The archive (`src/archive.rs`)
300
301A SQLite database, changed only by adding a step to `MIGRATIONS`. The tables
302share no keys, `events.browser` aside: an `asked` row's answers are what the `versions` behind it said,
303copied, and `counts` is a tally.
304
305`declined` is the questions Jev could not take (`NotAQuestion` and
306`NoQuestion`), so a link to one unfurls as "Please choose an LLM instead"
307with its own card (`Card::Declined`) and not as "Let me Jev that for you". It
308is apart from `asked` on purpose: nothing was answered, so none of them is
309counted, listed, toasted or votable. `asked` wins: a question that has since
310been answered is shown as answered.
311
312A question Jev declined can be voted on and commented on too, under the
313same thumbs and box. Its vote is keyed by `answer = 'declined'` (no hash of
314answers can be that word) and by the question, which is the `declined` row's
315key, so `ratings.input = declined.input AND ratings.answer = 'declined'`
316joins a vote to its record, and a comment to its vote as always. The thumbs
317mean other things there: up is "Jev was right to pass", down is "Jev could
318have answered". `about` on `ratings` and `comments`, and on both views, says
319which kind a row is (`answered` or `declined`); it is worked out from
320`answer`, never written. A question answered since is voted on as answered,
321and its earlier declined votes stay as they were.
322
323`askers` is how a question counts each browser once. A browser's first page
324gives it a random id in a cookie (`lmjtfy_browser`, a year); when it asks, the
325Worker hashes the id with the question and the archive keeps only that. A
326browser asking again, or opening its own question from the feed, finds its
327row and is not counted, toasted or moved up the feed again. The rows of two
328questions from one browser have nothing in common, and the id cannot be had
329back from one. So the key says nothing about who asked; the `browser` column
330beside it, which the owner's backend reads, is the id itself. An ask with no cookie (a
331script) counts every time, as before. Counts from before 2026-10-02 stay as
332they were.
333
334`ratings` holds the 👍 and 👎 under each answer ("Was Jev right?"): one vote
335per browser, keyed the same way, and pressed again to take it back. A vote is
336on the answer as it was kept when it was cast, by the hash of its answers, so
337if the question is answered differently later, that answer starts with no
338votes and the old one keeps its own.
339
340`comments` is what a visitor adds under their vote: the box that opens when
341they press 👍 or 👎 ("Comments?", and a grey "Let us know what we can improve"
342that goes as they type). It is a table of its own because `ratings` is the
343vote *now*, and a vote taken back is deleted, which would take the words with
344it; and because `events` is the request log, whose columns are what an
345`Event` says. Like `events` it takes `INSERT` and nothing else (a trigger
346refuses the rest): a row for every Save, with the vote as it was (`vote`)
347and the same key as `ratings` (`input`, `answer`, `who`). Saving the same
348words twice for the same vote keeps one row. What the visitor wrote is
349untrusted text from the public: it reaches the page only through maud's
350escaping, is in no log line and no event, and the owner's backend must show
351it as text, never as markup.
352
353The owner reads it through two views, which are not by the day and so have no
354finished copy: `vote_comments` is every comment (`comment`, `at_ms`, `input`,
355`browser`) beside the vote it was said of (`vote_then`) and the vote as it
356stands (`vote_now`, NULL if taken back), with `latest` for the last one of a
357vote; `votes_commented` is every vote now standing (`vote`) with its latest
358comment, NULL if it has none.
359
360```mermaid
361erDiagram
362  versions {
363    text sent_to PK "Jev's endpoint, or a Workers AI model id"
364    text request PK "the exact body sent"
365    integer version PK "1, 2, ...: each time it was sent, newest last"
366    text response "the exact body that came back"
367    text request_id "Jev's id for the call, if it gave one"
368    integer attempts "tries the client made"
369    real took_ms
370    real answered_ms "when it was answered"
371  }
372  asked {
373    text input PK "the question, cleaned"
374    text answers "JSON: one Answer per question Jev answered"
375    real asked_ms "last asked"
376    integer times "how often it was asked"
377    integer listed "1 if the feed may show it (Jev's fit fact)"
378    integer llm "1 if an LLM had to be asked"
379    integer moderated "the owner's say: 1 show, 0 hide, NULL leave it to listed"
380  }
381  declined {
382    text input PK "a question Jev could not take"
383    real declined_ms "last time"
384    integer times "how often"
385  }
386  events {
387    integer id PK
388    real at_ms "when"
389    text what "view, answer, gate, vote, comment, more, card, fetch, moved, live, left, read, out"
390    text method
391    text host
392    text path
393    text query "as it came"
394    text input "the question typed, asked or voted on"
395    text detail "how an ask ended, which way a vote went, clone or pull"
396    real status
397    real sent "calls sent for it"
398    real kept "calls answered from versions"
399    real llm
400    real took_ms "an answer's time, or how long a page stayed"
401    real first "1 on a browser's first page ever"
402    real daily "1 on its first page of the UTC day"
403    real session "1 on its first page in half an hour"
404    text referrer "the page that linked here, whole"
405    text source "utm_source or ref"
406    text client "browser, git, bot, other"
407    text family "Chrome, Firefox, ..."
408    text os
409    text device "mobile or desktop"
410    text language "Accept-Language"
411    text agent "User-Agent"
412    text browser "the lmjtfy_browser cookie"
413    text ip
414    text country
415    text region
416    text city
417    text postcode
418    text timezone
419    real latitude
420    real longitude
421    real asn "the network's number"
422    text network "whose network: the ISP"
423    text colo "Cloudflare's data centre"
424    text protocol "HTTP and TLS versions"
425    text medium "utm_medium"
426    text campaign "utm_campaign"
427    text screen "1920x1080, as the page says"
428    text viewport "the window"
429    real scroll "how far down the page was seen, 0 to 1"
430    text region_code "the region within its country: CA for California"
431    text continent
432    text metro "Cloudflare's metro area code, where it has one"
433    text bot "the kind of bot Cloudflare verified it as, if any"
434    integer day "days since 1970, worked out from at_ms, and indexed"
435  }
436  counts {
437    text name PK "sent, kept, clone:lmjtfy.git, pull:lmjtfy.git, ..."
438    integer n
439  }
440  askers {
441    text input PK "the question"
442    text who PK "SHA-256 of a browser's id and the question"
443    text browser "the browser's id itself, for the owner's reading"
444  }
445  ratings {
446    text input PK "the question"
447    text answer PK "SHA-256 of the answers as kept, or 'declined' for a question Jev declined"
448    text who PK "as in askers"
449    integer vote "1 right, -1 wrong (on declined: right to pass, could have answered)"
450    text browser "as in askers"
451    text about "answered or declined, worked out from answer"
452  }
453  comments {
454    integer id PK "in the order saved"
455    real at_ms
456    text input "the question"
457    text answer "as in ratings"
458    text who "as in askers"
459    text browser "as in askers"
460    text about "answered or declined, as in ratings"
461    integer vote "the vote it was said of, 1 right, -1 wrong"
462    text comment "the visitor's own words, up to 1,000 characters: text, never markup"
463  }
464  migrated {
465    integer version PK "MIGRATIONS steps run"
466    real at_ms "when the step ran, from step 18 on"
467    text build "the commit of the build that ran it"
468  }
469  finished {
470    text name PK "a view whose days that are over are copied into a table"
471    integer through "the last day copied, in days since 1970"
472  }
473```
474
475The questions the owner's backend asks are views here, not SQL in the
476backend. SQLite has no functions of one's own to define, so a question
477that would be a function of a day is a view with a `day` column, asked
478with `WHERE day = ...`. `events.day` is indexed, so asking of a day reads
479that day's events and no others.
480
481| View | What |
482| --- | --- |
483| `hits` | Every event, with what it counts as said once: `viewed` (a page given to a browser), `asked`, `answered`, `voted`, a `visitor` (the browser, or the address of one with no cookie), `came_from` (the site that linked here). |
484| `hours`, `days`, `today_by_hour` | The site's numbers for each hour and each day: views, visitors, sessions, new browsers, asks, answers, votes, clones, time read. |
485| `browser_days` | What each browser did on each day it came: viewed, typed, asked, voted, and whether it was its first. |
486| `outcomes_by_day`, `ask_endings_by_day`, `pages_by_day`, `questions_by_day`, `referrers_by_day` | A `label` and its number `n` for each day: what happened, how asks ended, pages viewed, questions asked, sites that linked here. |
487| `countries_by_day`, `cities_by_day`, `networks_by_day`, `agents_by_day` | The same, where `n` is how many different visitors: by country, city, network, and kind of browser. |
488| `country_visitors`, `city_visitors`, `network_visitors`, `agent_browsers` | Who those visitors were (`who`), a row each. The same visitor on two days is one visitor, so a stretch of days is counted from these: `COUNT(DISTINCT who)`. |
489
490A day that is over never changes. So each view by the day (all of those
491above but `hits`, `days` and `today_by_hour`, and the trees' below) is
492three things: `<name>_live` works it out from
493`events`, `<name>_finished` is a table the days that are over are copied
494into, once, and `<name>` is the two together, which is the one to ask.
495`finished` says how far each has been copied. The first event of a new day
496is what copies the day before (`Shelf::finish`).
497
498> **Aside.** Other databases call this a materialized view and keep it up to
499> date themselves. SQLite has none. But a view of a finished day has
500> nothing left to keep up with, so copying it once is the whole job.
501
502Two trees are for drilling into: the site's pages, and where visitors
503came from.
504
505| View | What |
506| --- | --- |
507| `pages` | A row per page with its views by people, browsers, views by bots and not-found answers, for all time. `/lmjtfy.git/apps/x` counts for `/lmjtfy.git`, for `/lmjtfy.git/apps` and for itself. `WHERE parent IS NULL` is the top of the site; `WHERE parent = '/lmjtfy.git'` is what is under it. |
508| `places` | The same for country, region, city and network: events, views, browsers and addresses for each place, with a region's `code` and the `latitude` and `longitude` of the middle of where its visitors were. `WHERE parent IS NULL` is the countries. |
509| `page_days`, `place_days` | A tree's rows for each day, kept as the views above are, for asking of a stretch of days. |
510| `page_visitors`, `place_visitors`, `place_addresses` | Who was on each page and from each place each day (`who`), for counting once over many days. `pages` and `places` are these added up. |
511| `vote_comments`, `votes_commented` | The comments under votes: every comment beside the vote it was said of and the vote now; and every standing vote beside its latest comment; both with `about` (`answered` or `declined`: up and down mean different things). Not by the day. |
512| `page_hits`, `place_hits` | Every event once for each level it belongs to, with its `at_ms`: for a question the others cannot answer, such as who read one page. They read `events`, so ask them of a stretch of time. |
513
514> **Aside.** SQLite has no function to split a path, and a Durable Object's
515> SQLite takes no functions of our own. So `page_hits` turns a path into a
516> JSON array (`/a/b` becomes `["","a","b"]`) and reads it back as rows with
517> `json_each`, which SQLite does have: a row per folder.
518
519`versions`, `events` and `comments` can be added to and nothing else: a trigger on
520each refuses a change or a deletion. What was kept stays as it was kept.
521
522It also holds, in memory only, the calls on the wire (so identical asks wait
523on one) and the `/live` sockets of every open page.
524
525### The budgets (`src/meter.rs`)
526
527Key-value storage, a JSON value per key.
528
529```mermaid
530erDiagram
531  neurons {
532    integer day "days since 1970, UTC"
533    real used "Workers AI neurons spent that day"
534  }
535  jev_dollars {
536    integer day
537    real used "dollars of Jev spent that day"
538  }
539  account {
540    integer day
541    real at_ms "when Cloudflare's figure was read"
542    real neurons "the whole account's neurons that day"
543  }
544```
545
546Each visitor's count for the minute (`budget::Visits`) is in memory and
547written nowhere by the site.
548
549## What is kept about visitors
550
551Everything a request says, for the owner alone. Until 2026-10-03 the site
552kept nothing about who asked; the owner then ruled the other way ("anywhere
553in the app where we are dropping data we should plug"), so this is the true
554account now.
555
556Every request worth keeping is a row of `events` in the archive
557(`packages/archive/src/event.rs`): a page viewed, an ask and how it ended,
558each as-you-type request, a vote, a clone, a redirect from an old address, a
559page connecting and leaving, and what each page reports of itself as it is
560left (how long it was in view, how far down it was read, the screen) or when
561a link off the site is followed. A row has the question, the visitor's address,
562their browser's id (the `lmjtfy_browser` cookie, which ties one browser's
563rows together), the user agent, the referring page, and what Cloudflare says
564of where the request came from: the network (the ISP), country, region,
565city, postcode, timezone and coordinates. The Worker writes the row after
566the response has gone (`wait_until`), so keeping it delays nobody.
567
568None of it is shown on the site. A question Jev judged unfit for the feed
569was always kept (`asked.listed`); now so is every question that ended any
570other way, in `events`.
571
572Visits are counted from a second cookie, `lmjtfy_visit`, which holds the
573time the browser was last counted: a page view compares it with now and
574marks itself the browser's first ever, first today, or first in half an
575hour.
576
577### The admin door
578
579The owner reads all of this from a separate, private Worker, which binds
580this Worker's archive object (`script_name` in its `wrangler.toml`) and
581posts `archive::Admin` messages to the object's `/admin` path:
582
583- `Select`: one SQL statement that only reads (`archive::reads_only`), and
584  its rows back, with how many rows SQLite read to find them. Cloudflare
585  meters rows read by the day, and a small answer can cost a whole table.
586- `Moderate`: the owner's say on whether a question is on the feed, kept in
587  `asked.moderated` beside Jev's own verdict in `listed`. The feed shows
588  `COALESCE(moderated, listed)`.
589
590- `Bookmark` and `Restore`: Cloudflare keeps thirty days of the archive's
591  history. `Bookmark` names a moment in it; `Restore` has the archive start
592  again as it was then, everything since lost, and answers with the
593  bookmark that undoes it. They are for a migration that went wrong, and
594  they work when nothing else does: an archive whose migrations did not
595  run refuses everything but these two. `migrated.at_ms` is when each
596  step ran, which is the moment to go back to just before.
597
598This Worker has no route that reaches that path. A visitor's request is
599passed to the object only as a socket upgrade on `/live`.
600
601### What Cloudflare keeps
602
603Cloudflare, which runs the site, keeps its own record, in the owner's
604account and nowhere public:
605
606- **Workers Logs**, for 3 days: one entry per request the Worker or its
607  objects handle, with the request as it arrived, address included.
608- **Traces**, for 7 days: each request's timing, through the fetches and
609  the Durable Object calls it made.
610- **Metrics**, the request counts and errors every Worker has.
611- **Web Analytics**: on `lmjtfy.fun`, Cloudflare adds its beacon script
612  (`static.cloudflareinsights.com/beacon.min.js`) to each page it serves to
613  a browser, and the browser reports page views and load times to
614  `lmjtfy.fun/cdn-cgi/rum`. It sets no cookie. The Worker's own HTML does
615  not contain the script: Cloudflare adds it on the way out, and only for
616  browsers, so `curl` does not see it.
617
618The owner turned on the logs and traces (2026-10-02) and Web Analytics with
619the domain (2026-10-03). They are configured in
620[wrangler.toml](wrangler.toml) under `[observability]`.
621
622## Credentials
623
624All from 1Password, none ever written to a file here. TypeSafe's API refuses
625browser origins anyway, so every Jev call goes through the Worker and the
626key never reaches a page.
627
628- **`LMJTFY_TYPESAFE_API_KEY`**, lmjtfy's own Jev key, is a Worker secret.
629  Under `wrangler dev` it comes from the process environment, which
630  `op-env-run` fills from the host's 1Password Environment. Deployed, it is
631  set with `lmjtfy-secret`. Without it the Worker still runs, and the page
632  says Jev is offline.
633- **The Cloudflare API token.** Workers AI has no local emulation, so even
634  `wrangler dev` calls the real models on the real account. `lmjtfy-wrangler`
635  (from the owner's devshell) reads the token from 1Password per run and
636  execs wrangler with it.
637- **`LMJTFY_GITHUB_TOKEN`**, a Worker secret: the clone proxy's GitHub
638  fine-grained token, Contents: read on every repository `Repo::ALL` serves (`src/clone.rs`)
639  and no other permission (Metadata: read, which the repository's description and topics
640  need, is implicit for a fine-grained token). GitHub's API cannot mint a fine-grained token, so
641  it is made on GitHub and kept in the host's 1Password Environment, which
642  `op-env-run` hands to `wrangler dev`; deployed, it is set with
643  `op-env-run -- lmjtfy-secret github`. Without it a clone is answered with 503.
644  Release files (`src/release.rs`) need the same token to read the repository's
645  **releases and their assets**, which Contents: read covers.
646- **`CLOUDFLARE_ANALYTICS_TOKEN`** and **`CLOUDFLARE_ACCOUNT_ID`**, Worker
647  secrets, are how the budget object reads the account's usage. The token can
648  read analytics and nothing else; nixos-config's infra declares it
649  (`cloudflare_account_token.lmjtfy-analytics`). The dev server runs without
650  them and counts only itself.
651
652## In this folder
653
654| Path | What |
655| --- | --- |
656| [src/](src/) | The Worker's code. Chapter 3 walks through it. |
657| [wrangler.toml](wrangler.toml) | The Worker's name, the pinned Jev model, the chosen LLM, the Jev budget, the bindings and the declared secrets; and the same again for `staging`, a second Worker with its own archive where a change is tried first. |
658| [build.rs](build.rs) | Stamps the build with the commit it came from (`LMJTFY_BUILD`). |
659| [Cargo.toml](Cargo.toml) | The crate: a `cdylib` for the Worker, and an `rlib` so its tests run natively. |
660
661`build/` and `.wrangler/` are the build's and the dev server's and are not
662committed.
663
664← Previous: [Chapter 1, apps/](../) · Up: [apps](../) · Next: [Chapter 3, the source](src/) →