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 <[email protected]>
API-dokumentationen har fulgt med hele vejen — der er en prøve, der fejler,
hvis en rute ikke står i openapi.yaml, og den fangede mig fire gange i går.
Brugervejledningen har ikke fulgt med, og det er den, folk læser.
Nyt: en side om beskeder — hvad der udløser en, hvorfor gentagne
rettelser bliver slået sammen, push pr. apparat og mærket på ikonet. En
om at åbne links i en anden browser, som mest af alt handler om, hvad en
webside ikke kan: hverken se hvilke browsere der findes eller vælge
mellem dem. Og en om tællingen af installationer, som ikke stod nogen
steder overhovedet — en melding, der er slået til som udgangspunkt, har
en pligt til at være skrevet ned, også når ingen har spurgt.
Rettet: AI-siden beskrev to funktioner, hvor der nu er syv, og
listen over hvad der sendes hvorhen var derfor forkert som en påstand om
noget privat. Noter-siden manglede deling med en e-mailadresse og hvad
`@Sofie` gør. Mail-siden manglede den anden vej ind i indbakken.
Konfigurationen sagde stadig, at versionstjekket kun kunne se efter.
Og tagbeskeden når nu frem. Den ER annonceringen — skrevet på produktets
eget sprog til dem, der bruger det — og udgivelsen på GitHub blev fyldt
med en autogenereret liste over commit-overskrifter i stedet, som er
skrevet til dem, der læser koden. Nu står teksten øverst og listen under
den. Linjen med linket til udgivelsen selv tages af: en side, der linker
til sig selv, hjælper ingen.
Dokumentationen bygges nu også på en PR. `mkdocs build --strict` fejler
på et link til en side, der ikke findes, og at bygge først efter merge er
at få det at vide på main.
Co-Authored-By: Claude Opus 5 <[email protected]>
A starred Gmail message became a task and kept its star, so a starred inbox only
grew — the IMAP side has always unflagged what it imports, and Gmail could not,
because the scope was gmail.readonly. This gives Gmail the same one write: after
the task is made, the STARRED label comes off the message, in one batchModify for
the whole sweep. Nothing else is touched — not the body, not other labels, and a
label trigger's label stays where it is, since that label is the person's own.
The scope goes from gmail.readonly to gmail.modify, which is the narrowest scope
Google offers that can remove a label. An existing connection keeps working on its
old read-only grant — the sync still reads, the unstar is a best-effort 403 that is
logged, not fatal — and starts clearing stars once the account is reconnected and
the new grant is in place. It self-heals: a mail that could not be unstarred stays
starred, stays in the next query, and is tried again.
The consent screen now asks for more than it used to; the settings copy, the docs
and the scope table all say modify rather than read-only in the same change.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The rate limiter and the audit log key on the caller's address, and that address
was read with chi's middleware.RealIP — which believes X-Forwarded-For from
anyone. So a caller could set it to a new value on every request and get a fresh
password-guess bucket each time, and could write any address they liked into the
log the panel's watchers read. The one control meant for when a password is
already gone was keyed on a number the attacker got to pick.
Now the header is believed only when the machine that *connected* is a trusted
proxy — by default anything on a private network, which is the shape of a reverse
proxy or tunnel sharing the host or the docker bridge. A connection straight off
the internet is trusted for nothing and is keyed on the address it actually came
from. VERDANDE_TRUSTED_PROXIES overrides the set (CIDRs, or "none" to trust no
proxy at all); VERDANDE_REAL_IP_HEADER names the header.
Verified in the current deployment before the change: behind the Cloudflare
Tunnel a spoofed X-Real-IP already got no fresh bucket, because cloudflared
overwrites it. This makes that safety a property of the code, not of the
deployment — expose the instance directly, or put a header-passing proxy in
front, and it would have been a live hole.
Also COOP: Cross-Origin-Opener-Policy: same-origin on every response, isolating
the browsing context. verdande scripts no other window, so there is nothing to
lose and a class of cross-window attacks to close.
Co-Authored-By: Claude Opus 5 <[email protected]>
`caldav.md` beskrev to måder at sende opgaver *ud*. Den tredje er den modsatte
retning og hører til samme sted, fordi det er dér, man ville lede.
`configuration.md` hed "Gmail" og handler nu om Google: samme klient, to
redirect-URI'er, to API'er, to scopes. Bemærkningen om en Internal-app står
begge steder, fordi den gælder begge veje — og for én privat konto er
kalenderens egen hemmelige ICS-adresse den kortere vej. Intet i verdande
abonnerer på sådan en endnu.
To nye sider. Postkasser: at stjerne i Gmail eller flage i iCloud og Fastmail
bliver til opgaver, at hver postkasse hører til den, der forbandt den, at
app-kodeord er noget andet end ens rigtige, og at Office 365 ikke kan nås ad den
vej — sidstnævnte skrevet ned frem for at blive opdaget ved at prøve. Og at logge
ind: kodeord, totrinskode og passkeys, med hvorfor en passkey ikke kan laves på
en bar IP-adresse.
Konfigurationen manglede VERDANDE_SECRET_KEY, som er den, det koster mest ikke at
vide noget om: mister man nøglen, skal postkasserne forbindes forfra. Begrundelsen
står med — det er kopierne, ikke serveren, der er problemet, for de kan hentes
gennem fladen.
Og genvejsafsnittet i quick-add beskrev q, t og u, som ikke findes længere. En
dokumentation, der lover taster, der ikke gør noget, er værre end ingen: man
prøver, der sker intet, og så tror man appen er i stykker.
Bygget med --strict; alle krydshenvisninger rammer.
Co-Authored-By: Claude Opus 5 <[email protected]>
A container cannot replace its own image from the inside, so "update" is really
"ask whoever owns the container to recreate it" — Yggdrasil pulls `:latest` on the
way, which is what makes a restart an update at all. This removes the second
browser tab, not the panel's authority: the panel still does the work and still
records who asked.
Three settings, needed together, and the page names whichever are missing — an
operator reading "not configured" should not have to go and find out which of
three. The token is never sent to a browser; the page is told only whether it is
set.
Answers 202 rather than waiting. The panel stops this container as part of
answering, so the reply may never arrive, and a request cut off mid-flight is the
successful case wearing a failure's clothes. The interface believes neither answer
and polls /healthz until it responds.
Administrators only, and sessions only. A leaked API token that could restart the
server is a denial of service in one request.
Co-Authored-By: Claude Opus 5 <[email protected]>
Gmail now connects. Authorisation-code flow with PKCE, the verifier kept
server-side rather than in a cookie — the callback is a top-level
navigation from Google, and a SameSite=Lax cookie is the kind of thing
that works until a browser tightens its defaults. State is checked, the
attempt expires after ten minutes, and the spent verifier is deleted
rather than left at rest.
Scope is gmail.readonly and the sync is one-way on purpose. Unstarring a
message does not delete the task; completing the task does not touch the
mailbox. Two-way would mean deciding what happens when both sides
changed, and the only honest answer is a sync model far larger than the
feature. The query carries newer_than:30d so connecting an account with
a decade of starred mail does not create a decade of tasks.
The update check is off unless the operator turns it on, asks GitHub at
most every six hours, and sends nothing about the instance — no
identifier, no version, no counts. A development build never reports an
update: somebody running their own tree is ahead of every release, and
telling them otherwise trains them to ignore the notice. Only
administrators see it, because only they can act on it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Material for MkDocs under docs/, covering self-hosting, the Rune, the
quick-add and filter languages, importing from Todoist, CalDAV,
mail-to-task, MCP, the AI layer and the API. The version is pinned: the
maintainers have announced that MkDocs 2.0 removes the plugin system
with no migration path, and an unpinned install would break this build
on the day it lands.
The landing page shows the quick-add parser as actual text rather than a
screenshot of it — that is the thing worth showing, and it reads better
as the real thing. It shares the app's design tokens, so the site and
what it describes look like one project.
MIT licence, contributing guide, security policy and issue templates.
Pages deployment is behind workflow_dispatch, so the docs build on every
push but nothing is published until the owner chooses to.
Co-Authored-By: Claude Opus 5 <[email protected]>