Commit Graph
7 Commits
Author SHA1 Message Date
kristianwindandClaude Opus 5 ff988d26bb feat(urd): mærket bliver ð, og frontend-bygget bliver deterministisk
## Mærket

Urds oplagte mærke var ᚢ uruz — bogstavet navnet begynder med i den ældre
futhark, præcis som Verdandes er ᚹ wunjo. Det blev tegnet sådan først.

Men uruz er allerede et af husets egne produkter, og dets egen
`scripts/gen-icons.ts` siger i sin kommentar at den "draws the Uruz rune (ᚢ)
in rune-gold" med "a tall left vertical, a shoulder across the top, and a
shorter" — altså samme glyf, samme konstruktion, kun en anden farve. To af
samme persons apps side om side på en hjemmeskærm, hvilket er nøjagtig det
sted mærket skal kunne skelnes.

ᚦ thurisaz blev forkastet et niveau nede af samme grund: en stav med en
trekant til højre, og det er ᚹ også. Ved 16px er de én form.

Så bogstavet frem for runen: ð, ethet i Urðr. Centreret ved at tælle malede
pixels frem for ud fra path-tallene, fordi en centreret kasse ikke er en
centreret glyf:

  verdande ᚹ   blæk x  53–108  midte 80.5  ·  y  18–131  midte 74.5
  urd      ð   blæk x  39–110  midte 74.5  ·  y  15–134  midte 74.5

Verdandes +5.5 i x er dens egen bevidste forskydning — en tung stav ved
siden af et let flag skal sidde til højre for midten for at se centreret ud.
ð er symmetrisk nok til at undvære den, og de to er enige i y.

Læsbarhed, målt: ð læses fra 32px og op. Ved 16px er det en klat, og det er
ᚹ også — en favicon er ikke det, der skiller to installerede apps i en dock.

`productRune` hedder nu `productMark`, fordi ð ikke er en rune.

## Determinisme

SvelteKits `kit.version.name` er som standard `Date.now()`. Millisekundet
landede i version.json, i index.html og i service-workeren, ændrede
indholdshashen på hver chunk der refererer dem, og dermed deres filnavne. To
bygninger af samme commit havde forskellige filer i sig.

Dockerfilen har i et år sagt at `-trimpath` og en tom buildid "keep the
output reproducible". Flagene gjorde deres arbejde; frontenden i binæren bar
et tidsstempel. Sætningen var falsk, og ingen læste den.

Fastgjort til VERSION, som udgivelsen allerede sender ind, og `dev` når
intet sætter den. Målt ved at bygge to gange og diffe træet, før og efter.
Prisen er at et lokalt `npm run build` ikke længere ligner en ny version for
service-workeren — hvilket kun betyder noget på en udrullet instans, hvor
udgivelsen sender det rigtige mærkat.

Det gør også CI's nye påstand mulig: de to images deler alle lag, så urd
bærer samme binær som verdande frem for en genbygning der tilfældigvis
matcher.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 10:10:30 +02:00
kristianwindandClaude Opus 4.8 57689ddfbb feat(offline): hold through a short network drop with a caching worker
The service worker did Web Push and nothing else, with a note that a cache
serving yesterday's data was worse than an honest "cannot connect". This adds a
cache that does not do that: network-first for everything that can go stale, so a
reader online always gets the live answer and the cache is reached only when the
network cannot answer. The task and note lists stay live; they just no longer go
blank the moment the signal does — a reload through a tunnel, a lift or a
base-station handover comes back to the app instead of the browser's offline page.

Writes are untouched and still go straight to the network; api.js already retries
them across the same short drop. The shell cache is versioned so a deploy lands in
a fresh cache and the old one is dropped on activate; the data cache is cleared on
sign-out so the next person on the browser does not inherit it.

The old hand-registered /sw.js becomes SvelteKit's src/service-worker.js, which it
registers itself, and the push handlers move across unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-03 22:46:35 +02:00
kristianwindandClaude Opus 4.8 0363d5f422 feat(icon): centre and strengthen the app icon
The Wunjo rune sat left of centre and thin. It is heavier now (a 6.5
stroke), bigger, and nudged right of the geometric centre so the heavy
stave and the light flag balance about the middle of the tile — so it
reads clearly as a home-screen icon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-03 14:53:21 +02:00
Kristian WindandClaude Opus 5 4be934ccc1 feat(pwa): the PNG icons the manifest and iOS have always wanted
The manifest pointed at icon.svg and nothing else. Chrome and Firefox
install happily on that; Safari does not read the manifest's icons at all
and looks for /apple-touch-icon.png by name, so an installed PWA on iOS got
a screenshot of the page as its home-screen icon.

This was written down as blocked: there is no SVG renderer on this machine,
which is why the references were removed rather than left pointing at files
that did not exist. Playwright is an SVG renderer, and it has been a
dependency the whole time.

scripts/generate-icons.js renders the same SVG at 192, 512 and 180. A script
rather than a test, so CI never runs it and never rewrites a tracked binary
— run it when the icon changes. The smoke test already fetches every icon
the manifest names, and now asks for the Apple one by name too, so a
reference with no file behind it fails there rather than on somebody's
phone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:37:52 +02:00
Kristian WindandClaude Opus 5 1bb28ce862 feat(web): the service worker, and subscribing to Web Push
The server side has been finished for a while — VAPID keys, RFC 8291
encryption, delivery on reminders and assignments — and nothing in the
browser ever asked for a subscription. web/static/ had a manifest and an
icon and no sw.js, so a subscription could not be created at all.

The worker handles push and notificationclick and nothing else. There is
deliberately no offline cache: the app is a live view of a list other
people are editing, and a cached shell serving yesterday's data is worse
than the browser saying it cannot connect. Adding one later means
versioning and invalidating it, which is a feature rather than a few
lines here.

Unsubscribing tells the server first. If the browser unsubscribed and
then the request failed, the server would keep pushing to an endpoint
nothing is listening on, and only a 410 from the push service would ever
clean it up.

Still not verified against a live push service — this is the client half
of a chain whose other half has only ever been tested against the RFC.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 10:56:33 +02:00
Kristian WindandClaude Opus 5 2a3367bfe0 fix(web): the label route was written outside the routes tree, and the PWA promised icons that do not exist
Release / Build and publish image (push) Failing after 13m28s
Every label link in the sidebar was dead. The route file landed in
web/web/src/routes/ because the shell that created it was already inside
web/ — SvelteKit never saw it, so /etiket/<name> fell through to the
app shell and rendered nothing.

The manifest also listed icon-192.png and icon-512.png, and app.html an
apple-touch-icon, none of which were ever generated. A manifest pointing
at missing icons is worse than one listing only what exists, so the dead
references are gone and the SVG — which Chrome and Firefox install from
happily — is what remains.

docs: hand over the project

HANDOFF.md covers what cannot be read out of the code: why the name is
not skuld, the decisions that look strange until you know the reason,
what is tested against a real service and what is only tested against
its specification, the gaps in the order I would take them, and the
traps that cost the most time to find.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 08:47:29 +02:00
Kristian WindandClaude Opus 5 12a9597919 feat(web): SvelteKit PWA with the Nordic design system
The interface for phase 1: sign-in and first-run setup, Today, Upcoming,
project views, sharing, Cmd+K search and the keyboard shortcuts.

Quick add highlights what the parser understood underneath the text as
you type — a mirrored layer behind a transparent input, sliced by byte
offset because the server counts bytes and JavaScript counts UTF-16
units, which disagree on exactly the Danish letters this app is full of.

Every local action is optimistic and reconciled afterwards: ticking a
task off strikes it through and animates it out before the request
leaves, and rolls back with a message if the server disagrees.

The Content-Security-Policy is computed at startup by hashing the inline
scripts in the built index.html. Both of them change on every frontend
build, so a hard-coded hash would go stale silently and show up only as
a blank page in production. The alternative, 'unsafe-inline', would drop
the one protection that makes an injected script in a task title inert.

Today is derived from the store by date rather than from the ids that
came back with the first request — filtering against a captured list
meant a task added through quick add never appeared until a reload.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 00:03:20 +02:00