Verdande og Urd er to af nornerne i Völuspá, og digtet har dem skære
mærker i træ: *skáru á skíði*. Verdande er det der bliver; Urd er det der
blev, og blev gemt. En instans startet uden opgaveruterne er et andet
produkt for den der bruger den, så den siger et andet navn.
Navnet læses fra udgaven i ÉT sted — Config.ProductName/ProductSlug —
frem for at stå skrevet i teksterne. Frontend-bygget er byte-identisk i
begge udgaver; serveren omdøber de to filer, browseren læser før noget
JavaScript kører.
Der er to titler, og det blev først klart, da e2e-prøven sagde det:
skallens <title>, som serveren omdøber, og rutens egen i <svelte:head>,
som appen sætter bagefter og som overskriver den første. Go-prøven var
grøn over de bytes, serveren sendte, mens fanen sagde "Noter · verdande".
Begge prøver spørger nu om begge.
Omdøbningen er ankret på hele strenge frem for på ordet "verdande", fordi
app.html også indeholder localStorage.getItem('verdande:theme'). Omdøbes
præfikset der og ikke i resten af appen, bliver temaet sat fra én nøgle og
læst fra en anden, og det eneste symptom er det hvide blink, det
indlejrede script findes for at undgå. Tre plantede fejl bekræfter, at det
er de rigtige prøver, der fanger det: et flyttet anker i app.html, en bred
søg-erstat, og en applyRebrands uden fejlretur.
Mailen var det vigtigste sted. appName var et felt med en værdi og ingen
læser, og kommentaren ved siden af sagde, at det stod i hver besked — de
tre steder havde ordet skrevet ud i stedet. En invitation fra en
urd-instans går til et andet menneskes indbakke.
Også omdøbt: web-manifestet (og sendt som application/manifest+json — Go
kender ikke .webmanifest og sniffer den til text/plain, hvorefter
browseren ignorerer manifestet uden at sige noget, og installationsknappen
bare ikke er der), mærket i menuen (ᚢ uruz frem for ᚹ wunjo),
opstartsloggen, passkey-promptens visningsnavn, ICS-kalendernavnet og
udseendet, der er opkaldt efter programmet.
Ikonet er IKKE med: icon.svg er stadig ᚹ, og de tre PNG'er genereres fra
den. Det rører binære, genererede filer og hører i sin egen ændring.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3.8 KiB
The install count
verdande reports that it exists — and so does urd, which is the same program in its notes edition. Two values, once a day:
{
"instance_id": "01a0806d-3601-7000-ad76-057f1f684881",
"version": "v0.44.1"
}
That is the whole message. No IP is stored by the collector, no hostname, no domain, no account, no counts of anything you have made, nothing about what you use it for — and not which of the two editions is running, so the count does not tell the two apart either. The payload is small enough to be printed in full on the settings page, and it is printed there — a promise about telemetry is worth exactly as much as your ability to check it.
On by default, off in one click
The alternative that was considered first was compulsory, and the argument against it is practical rather than principled: it cannot be enforced. The domain can be blocked, the binary can be patched, and a fork removes it in its first commit. So mandatory buys nothing over on-by-default while costing the trust the rest of this program works for.
Which is why the first administrator to sign in is told, unprompted, with the real id, the real version and the real address in front of them, and two buttons. On by default is only defensible if it is said out loud without being asked; a settings page where it is stated fully is not the same thing, because that page is opened by people who already suspect there is something to read.
Settings → Data afterwards: one switch. An explicit "off" survives upgrades.
!!! note "This disagrees with the update check, on purpose"
`VERDANDE_UPDATE_CHECK` is off unless you ask for it, and its reasoning is that
a self-hosted app reaching out unbidden has broken the deal its operator made by
self-hosting. That is the same situation as this one, and the two defaults do
not agree. The case for treating them differently: the update check reveals an
instance to a third party (GitHub) and returns something you can act on, while
this returns nothing and goes to the project. Whether that is a distinction or
an excuse is a judgement call, and it was made deliberately.
What the collector cannot see, and what it can
A request has a source address whether or not anybody writes it down. The collector stores none and logs none, and the code is arranged so the address never reaches a function that could — the table has nowhere to put one. But the request passes through a reverse proxy and a tunnel on the way, and those keep their own logs. "We send no IP" would be a half-truth; what is true is that nothing here records one.
The ping is not authenticated either, so anybody can post a made-up id and inflate the count. That is an accepted trade: a token would have to ship inside every install anyway.
Running your own count
Somebody running a fleet points Settings → Data → Modtagerens adresse at their
own instance, and ticks Denne instans er tælleren there. Until that is ticked,
POST /api/v1/beacon answers 404 rather than accepting quietly — an instance
nobody made a collector must not become a bucket anyone can post into.
The collector's page then shows totals, active in 7 and 30 days, and a breakdown by
version. Publishing that number at /api/v1/beacon/count is a second, separate
opt-in, and it has a floor under it (25 by default): a number that falls cannot be
published as a fact — a bad release, a DNS change that quietly stops the pings, or
lost rows all look like "fewer people use this now" — and a small number mostly says
how little it takes to move it.
When it cannot reach
The reason is written down and shown on the settings page. A beacon that cannot reach its collector otherwise fails in complete silence: it tries again tomorrow, the page says "last sent" with a date that quietly gets older, and nobody notices.