Commit Graph
351 Commits
Author SHA1 Message Date
kristianwindandClaude Opus 5 9b21b3eac5 feat(projekter): del et projekt med en, der allerede er her
Et projekt kunne kun deles ved at stave en e-mailadresse. Det er den forkerte
vej at spørge om en kollega, man kender navnet på og sidder i samme rum som —
og beslutningen om, hvorvidt en instans' konti må vises for hinanden, var
allerede taget: `UsersForSharing` har gjort det for noter hele tiden, med
begrundelsen skrevet ved siden af. Der er ingen åben oprettelse, så konti er en
lukket, inviteret mængde, og det er en adressebog frem for en udlevering.
Projekterne bruger nu den samme.

`GET /members` svarer med tre lister frem for én, så panelet åbner færdigt i ét
kald — samme form som notens delingspanel. `POST /invites` tager `user_id` ved
siden af `email`, og de to veje mødes ét sted: en adresse, der viser sig at høre
til en konto, ER den konto, og skal ikke gennem en oprettelsesside, den ikke kan
komme igennem.

De ventende invitationer kom med, fordi den nye liste skabte behovet for dem:
den, der er inviteret og ikke kommet, har ingen konto og står derfor hverken
blandt medlemmerne eller blandt dem, man kan vælge. Før betød det, at man ikke
kunne se sin egen invitation; nu ville det betyde, at man leder efter en person,
der mangler i listen, og inviterer igen. Det andet link virker lige så godt som
det første. De kan trækkes tilbage, for ellers er den ventende liste kun til at
se på.

Tre ting, der kostede en kørsel hver:

`OpenAPI is valid` fangede den nye DELETE-rute, før jeg selv gjorde.

To felter hed "Rolle" på den samme side. Det er ikke kun en prøve, der brokker
sig — en skærmlæser siger "Rolle" to gange uden at kunne skelne. De har nu hver
sin tilstand og hvert sit navn, og den gamle prøves `getByLabel('Rolle')` er
blevet `exact`, fordi den matchede som understreng.

Og e2e-prøven lånte først sin anden person fra prøven ovenfor. Det så jeg kun,
fordi jeg kørte den alene: basen tømmes for hver kørsel, så en prøve, der kun
består, når hele filen kører i rækkefølge, består ikke af sine egne grunde. Den
laver nu selv sin — og viser dermed skiftet: den samme person er først en
ventende adresse og bagefter et navn, der kan vælges.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 17:52:03 +02:00
Kristian Wind a39f655565 Merge pull request #75 from kristianwind/soeg-i-indstillinger
CI / OpenAPI is valid (push) Successful in 9s
CI / End-to-end (push) Successful in 8m38s
CI / Docker image builds (push) Successful in 1m38s
CI / Go (push) Failing after 14m6s
Release / Build and publish image (push) Failing after 4s
feat(indstillinger): en indstilling kan findes ved at søge efter den
v0.45.0
2026-09-12 10:04:17 +02:00
kristianwindandClaude Opus 5 7e981354c2 feat(indstillinger): en indstilling kan findes ved at søge efter den
Indstillingerne er ti faner med syvogtredive afsnit, og den eneste vej til et
bestemt af dem var at huske, hvilken fane det lå under. ⌘K kunne opgaver,
projekter og noter — alt det, man selv har skrevet, og intet af det,
programmet består af.

Ét indeks, to steder det bruges: et felt øverst i indstillingerne og paletten.
To lister over de samme afsnit ville være to steder at glemme et nyt et.

Overskrifterne står ikke i indekset. Hver post peger på den nøgle, afsnittets
egen overskrift allerede bruger, så et afsnit, der bliver omdøbt, følger med af
sig selv, og søgningen kan begge sprog uden at kende nogen af dem.

`words` er derimod skrevet i indekset, og det er med vilje: det er søgeord,
ingen får at se. Det danske afsnit hedder "Tælling af installationer", men det
ord, man kender det ved, er "beacon" — og på samme måde 2fa, imap, webcal,
webhook. De er de samme på begge sprog, hvilket er hele grunden til, at de
virker som søgeord.

Ankrene er nye id'er på afsnittene. Tre af dem stødte sammen med et felt inde i
afsnittet — to ens id'er gør <label for> tvetydig og getElementById tilfældig —
og hedder derfor to-faktor, kalenderfeed og caldav-server.

Springet dertil kan ikke overlades til browseren: siden hentes af ruteren, og
afsnittet findes først, når dens data er hjemme. Der prøves i to sekunder og
gives så op. Blinket bagefter er ikke pynt — man er landet midt i en side med
otte afsnit, man ikke selv rullede hen til.

Prøverne læser filerne frem for at klikke: halvdelen af afsnittene vises kun
under en betingelse, så en prøve, der ledte efter dem i en browser, ville melde
fejl på det, der med rette var skjult.

Og palettens pladsholder sagde "Søg i opgaver og projekter". Den har kunnet
noter længe, og kan nu også indstillinger.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 09:56:00 +02:00
Kristian Wind 1f0a43ac90 Merge pull request #74 from kristianwind/kalender-url-ud-over-kanten
CI / OpenAPI is valid (push) Successful in 9s
CI / End-to-end (push) Successful in 8m13s
CI / Docker image builds (push) Successful in 1m30s
CI / Go (push) Failing after 13m56s
fix: URL'er kan læses helt på en telefon
2026-09-12 09:43:40 +02:00
kristianwindandClaude Opus 5 61f8887319 fix(integrationer): en adresse kan læses helt, også på en telefon
Feed'et, mailadressen, krogen, curl-eksemplet, CalDAV-serveren og de to
redirect-URI'er lå i skrivebeskyttede <input>. Et input ombryder ikke — det
ruller indeni — så på en telefon stoppede adressen midt i kassen, og resten
fandtes kun ved at trække inde i feltet. Der var ikke engang et rullepanel at
se: feltet så ud som om det indeholdt det, der stod i det. Målt ved 390 px var
kassen 306 px og krogens adresse 634.

De er nu <output>. Det er stadig et felt, en <label for> kan pege på, så
navnene på felterne bliver ved med at høre sammen med dem, og det er tekst,
der ombryder. `user-select: all` beholder det ene, inputtet gjorde bedre: ét
tryk tager hele adressen.

Det samme gjaldt invitationslinket i en delt note, som vises netop når der
ikke er nogen post at sende det med — altså når det er det eneste, der findes.

Prøven nævner felterne ved navn frem for at forespørge på en klasse. Den, der
måler `.field output` og finder nul, består også den dag felterne er lavet om
til noget andet klippet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 09:18:49 +02:00
kristianwindandClaude Opus 5 72637a153a fix(kalender): en abonnementsadresse skubber ikke siden ud over skærmen
Adressen på et kalenderabonnement blev vist i en punktliste uden noget, der
lod den ombryde. En URL er ét ord på et par hundrede tegn uden et mellemrum i,
så den kan ikke bryde af sig selv — og det bredeste element på en side
bestemmer sidens bredde. På en telefon betød det, at hele indholdsspalten fik
et vandret rullepanel, ikke bare den ene linje.

`overflow-wrap: anywhere` er det, resten af indstillingerne allerede bruger til
et token og en hemmelighed; listen her var den, der blev glemt. Samtidig får
den layout frem for at ligge som løse inline-spans: navn, adresse og knap
under hinanden, uden punkttegnets indrykning.

Prøven måler `.content` og ikke dokumentet. Skallen holder selv siden på
skærmens bredde, så en måling på documentElement består med fejlen i behold —
den blev skrevet først og bestod, hvilket er grunden til at kommentaren står
der.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 09:11:26 +02:00
Kristian Wind f06a25b43e Merge pull request #73 from kristianwind/dagens-plan-kan-ses
CI / OpenAPI is valid (push) Successful in 9s
CI / End-to-end (push) Failing after 10m29s
CI / Docker image builds (push) Successful in 1m54s
CI / Go (push) Failing after 14m45s
feat(plan): dagens plan står på I dag, og ikke kun i en besked
2026-09-09 10:32:28 +02:00
kristianwindandClaude Opus 5 6cf184caa6 feat(plan): dagens plan står på I dag, og ikke kun i en besked
Planen var en notifikation og ikke andet. Klokken klippede den af efter
én linje — "2 forfalder i dag, 1 er sprun…" — og klikket førte ingen
steder hen, fordi en plan hverken er en note, en opgave eller et projekt.
Det eneste sted, den landede hel, var push-beskeden på telefonen.

Nu står den som et kort øverst på I dag, dér hvor man kigger, når man
starter dagen. Beskeden er påmindelsen; kortet er stedet, den bliver.

Teksten er gemt for dagen frem for regnet forfra, hver gang nogen kigger.
En model pr. sidevisning er dyr for noget, der ikke har ændret sig — og
vigtigere: en plan, der siger noget andet, når man kigger på den, end den
sagde, da den kom, er værre end begge dele hver for sig. Kortet siger
også, hvornår den blev lavet: en plan fra klokken syv er en anden slags
oplysning klokken fire end klokken otte.

En plan fra i går sendes ikke med. Et kort, der viser gårsdagens plan
under overskriften "i dag", er værre end et tomt kort.

"Forny planen" laver en ny, når dagen har flyttet sig, og sender ikke en
besked om noget, man står og kigger på — den slags lærer folk at ignorere
klokken. Og for den, der har morgenbeskeden slået fra, tilbyder kortet at
lave planen på stedet, så funktionen ikke kun findes for dem, der vil
vækkes.

Klokken fører nu til I dag, og planen fylder de tre linjer, den er, i
stedet for at blive klippet af.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 10:22:27 +02:00
Kristian Wind 3c14906db2 Merge pull request #72 from kristianwind/dokumenter-det-der-blev-bygget
CI / OpenAPI is valid (push) Successful in 9s
CI / End-to-end (push) Successful in 9m44s
CI / Docker image builds (push) Successful in 1m50s
CI / Go (push) Failing after 14m28s
docs: skriv det ned, der er blevet bygget
2026-09-09 09:02:58 +02:00
kristianwindandClaude Opus 5 70d7398d5a docs: skriv det ned, der er blevet bygget
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 <noreply@anthropic.com>
2026-09-09 08:52:54 +02:00
Kristian Wind 1cabbb7a78 Merge pull request #71 from kristianwind/hvilken-browser-aabner-linket
feat(links): vælg hvilken browser et link åbner i
2026-09-09 08:50:30 +02:00
kristianwindandClaude Opus 5 5eb689e4c2 feat(links): vælg hvilken browser et link åbner i
Ønsket var en liste over de installerede browsere. Den findes ikke: en
webside kan hverken se, hvilke browsere der er på maskinen — listen er
lukket land, fordi den er et fingeraftryk, der ville følge folk rundt på
nettet — eller bede systemet om at bruge en bestemt af dem.

Det eneste håndtag er de URL-skemaer, browserne selv registrerer:
firefox://open-url, googlechrome://, microsoft-edge:. Dem kan man prøve,
og man kan ikke spørge, om de findes. Så indstillingen er skrevet som
det, den er — et forsøg med et faldback — frem for som et løfte, der
ville holde på telefonen og ikke på en Mac.

Valget står i localStorage, ikke på kontoen. Samme grund som
sidebjælkens bredde: det er en egenskab ved maskinen, man sidder ved.
firefox:// virker på telefonen og gør ingenting på Mac'en, så et valg,
der fulgte kontoen, ville være forkert det ene af de to steder hver gang.

Ét sted i koden frem for ét pr. komponent. Links står i opgavetitler, i
beskrivelser, i noter og i indstillingerne, og en regel, der skulle
gentages hvert af de steder, ville mangle det ene sted, ingen tænkte på.
Kun udadgående links; programmets egne ruter går gennem SvelteKit som før.

To ting, målingen rettede undervejs:

Det skjulte iframe — den kendte opskrift — bliver blokeret af appens egen
Content-Security-Policy, som siger default-src 'self'. Uden e2e-prøven,
der fangede CSP-fejlen i konsollen, ville skemaet aldrig være blevet
forsøgt i produktion, og indstillingen ville have set ud til at virke,
fordi faldbacket åbnede linket alligevel. Nu sættes location i stedet:
et ukendt skema afbryder navigationen frem for at forlade siden.

Og faldbacket troede sig blokeret hver eneste gang. window.open med
noopener returnerer altid null — det står i specifikationen — så
kontrollen af, om fanen blev åbnet, faldt altid ud til "blokeret", og
den aktuelle fane blev navigeret væk i stedet. Håndtaget er det eneste,
der kan kende en blokeret fane fra en åbnet, så det bruges nu, og opener
ryddes bagefter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 08:41:21 +02:00
Kristian Wind e931ddc313 Merge pull request #70 from kristianwind/ny-version-siger-til
CI / OpenAPI is valid (push) Successful in 11s
CI / End-to-end (push) Successful in 9m50s
CI / Docker image builds (push) Successful in 1m54s
CI / Go (push) Failing after 14m45s
Release / Build and publish image (push) Failing after 5s
feat(opdatering): en ny version siger til, i stedet for at vente på at blive fundet
v0.44.1
2026-09-08 17:05:52 +02:00
kristianwindandClaude Opus 5 2210dc082a feat(opdatering): en ny version siger til, i stedet for at vente på at
blive fundet

Tjekket fandtes i forvejen, og svaret stod ét sted: nederst på
indstillingssiden for beskeder. Det er en oplysning, man kun får, hvis
man i forvejen havde mistanke om, at der var noget at se efter — og "der
er kommet en rettelse af noget, du er ramt af" er ikke noget, man gætter
sig til at kigge efter.

Nu går den i klokken, i push og på mærket på ikonet, som alt andet, der
er værd at vide. Og den fører hen, hvor knappen til at genstarte sidder,
frem for ud af programmet.

Én besked pr. version, ikke én pr. time. En påmindelse hver time om det
samme er den slags besked, folk slår fra — og så er den næste, der
betyder noget, også slået fra. Versionen, der er sagt til om, skrives
først, når beskederne er sendt: den anden vej rundt ville en fejl
midtvejs betyde, at halvdelen fik besked og resten aldrig gjorde.

Kun administratorer, af samme grund som versionsfeltet allerede er det:
en besked om, at serveren er bagud, er en besked om en andens arbejde,
hvis man ikke selv kan gøre noget ved den.

Og et tjek, operatøren har slået fra, bliver ikke til en besked ad
bagvejen. VERDANDE_UPDATE_CHECK styrer det hele, som før.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 16:55:53 +02:00
Kristian Wind 9fa47822c1 Merge pull request #69 from kristianwind/ai-dagens-plan-2
CI / OpenAPI is valid (push) Successful in 11s
CI / End-to-end (push) Successful in 10m57s
CI / Docker image builds (push) Successful in 2m19s
Release / Build and publish image (push) Failing after 7s
CI / Go (push) Failing after 15m3s
feat(ai): dagens plan, mails der giver mening, og svar på et spørgsmål
v0.44.0
2026-09-08 15:15:48 +02:00
Kristian Wind 366bed3a15 Merge pull request #68 from kristianwind/ai-foreslaar-du-bestemmer
feat(ai): forslag, man siger ja til — indbakken og noternes opgaver
2026-09-08 15:06:21 +02:00
Kristian Wind 07fc170b91 Merge pull request #67 from kristianwind/beacon-siger-det-hoejt
feat(beacon): sig det uopfordret, og lad være med at fejle i tavshed
2026-09-08 15:06:17 +02:00
kristianwindandClaude Opus 5 8139003b35 feat(ai): dagens plan, mails der giver mening, og svar på et spørgsmål
De tre sidste af de fem. Alle tre samme sted hen: det, man ellers selv
skulle sidde og gøre klokken syv om morgenen.

Dagens plan. Én besked om morgenen om det, der forfalder i dag, og det,
der er sprunget over. Tallene tælles i basen og gives til modellen som
kendsgerninger, den ikke må regne om — en plan, der siger fire opgaver,
hvor der er tre, er værre end ingen plan. Er der ingen model, står
tallene alene, og det er stadig det, man havde brug for at vide. Er der
hverken noget, der forfalder, eller noget, der er sprunget over, kommer
der ingen besked: en morgen uden noget at gøre er en god morgen, og en
besked om ingenting lærer folk at lade være med at åbne beskeder.

Timen er personens egen — klokken syv er syv forskellige steder — og
jobbet kører hver time frem for på et klokkeslæt, så en instans, der sov,
ikke springer dagen over. Flytter man timen, ryger dagens mærke, så man
kan se virkningen i dag i stedet for i morgen.

Mails, der giver mening. Opgaven hed, hvad mailen hed: "Anders Jensen:
SV: SV: Vedr. levering uge 12". Nok til at genkende, ikke nok til at
gøre noget ved. Nu skriver modellen den handling, mailen beder om, og
lader være, hvis den ikke beder om nogen. Ét kald pr. gennemløb frem for
ét pr. mail — femogtyve mails er ellers femogtyve gange ventetid — og
emnet bliver stående i beskrivelsen, så en misforstået mail stadig kan
læses. Falder modellen ud, hedder opgaven afsender og emne som før: en
model, der er nede, må ikke kunne stoppe posten.

Og svar på et spørgsmål. "Hvad lovede jeg Anders i august?" — svaret står
spredt over tre noter, og ingen af dem indeholder ordet "lovede".
Søgningen kræver, at alle ord optræder, hvilket er rigtigt for et
søgefelt og forkert for et spørgsmål, så kandidaterne hentes ord for ord
og de noter, flere ord rammer, kommer øverst. Modellen får kun dem: den
slår ikke selv noget op, så et svar kan ikke handle om nogen andens
noter. Og svaret bærer, hvad det står på — en model, der siger "det
gjorde du den 14.", er kun brugbar, hvis man kan slå op i det, den læste
det i.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:05:36 +02:00
kristianwindandClaude Opus 5 96e1fa251b fix(beacon): beskeden skubber i stedet for at dække
Den lå som en rude i hjørnet, oven på arbejdet, og dækkede knapperne i
notens fod og i delepanelet — seks e2e-prøver klikkede forgæves på noget,
der lå under den.

Det var ikke kun et prøveproblem. En besked, der er i vejen, bliver
klikket væk uden at blive læst, og så er den uopfordrede oplysning en
formalitet frem for noget, nogen har set. Nu står den øverst i indholdet
og skubber siden ned, indtil den bliver besvaret.

Og e2e beviser den nu frem for at klikke den væk: den første konto skal
møde beskeden, og der skal stå et instance_id og en version i den.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 14:51:47 +02:00
kristianwindandClaude Opus 5 40102d6038 feat(ai): forslag, man siger ja til — indbakken og noternes opgaver
To steder, hvor arbejdet ligger og venter på at blive skrevet færdigt.

Indbakken er der, hvor ting lander, når man ikke havde tid til at skrive
dem ordentligt — og derfor også der, hvor de bliver liggende. At åbne
hver enkelt og sætte projekt, dato og prioritet er ti klik pr. linje. Nu
er det ét spørgsmål og et ja pr. linje.

Og et referat er fuldt af sætninger, der ligner opgaver, og af tre, der
er det. Modellen bliver bedt om kun at tage dem, nogen har forpligtet sig
på, og om at citere de ord, den tog dem fra — så et forkert punkt kan
gennemskues frem for bare at være forkert.

Modellen skriver ikke noget. Den foreslår en *linje* i quick-add-syntaks,
og forslaget bliver til noget, når nogen siger ja. Det er ikke
forsigtighed for forsigtighedens skyld: en model, der retter i folks
opgaver direkte, skal have ret hver gang for at være til gavn, hvor en,
der foreslår, skal have ret tit nok til at spare et par tastetryk.

Linjen er quick-add-syntaks frem for et felt pr. egenskab, fordi parseren
findes. Der er én fortolkning af "i morgen" i dette program, og forslaget
går gennem den samme som alt andet — også når linjen er blevet rettet i
hånden, inden den blev accepteret. Og et forslag, skrevet i det sprog man
selv skriver i, kan læses og rettes af den, det bliver stillet til.

Modellen får listen over de projekter, der findes, og må ikke nævne
andre. Et `#Nyt projekt`, den fandt på, ville lave et projekt ved et
uheld, første gang nogen sagde ja — og et gæt må ikke kunne flytte en
opgave hen, hvor ejeren ikke leder efter den. Testet.

Forslagene til indbakken bærer et nummer tilbage til den opgave, de
handler om. Rækkefølgen alene ville også kunne bruges og gør det som
reserve, men en model, der springer en opgave over, ville så flytte alle
de følgende forslag én plads — og "sæt en dato på den her" ville lande på
en anden.

Noten bliver ikke rørt, når der laves opgaver ud af den. Det er
fristende at skrive linket ind i den, så de to peger på hinanden, men en
tekst, et program skriver i uden at blive bedt om det, er en tekst, man
holder op med at stole på. Opgaven bærer i stedet, hvor den kom fra.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 14:48:56 +02:00
kristianwindandClaude Opus 5 0cf5c8c281 feat(beacon): sig det uopfordret, og lad være med at fejle i tavshed
Beaconet er slået til som udgangspunkt. Det kan kun forsvares, hvis det
bliver sagt af sig selv — og det blev det ikke. Det stod rigtigt og
fyldestgørende under Indstillinger → Data, som er en side, der bliver
åbnet af dem, der i forvejen har en mistanke om, at der er noget at læse.
For alle andre var "du kan slå den fra" et tilbud til nogen, der ikke
vidste, der var noget at slå fra.

Nu møder en administrator beskeden første gang: hvad der bliver sendt,
med de rigtige værdier i — det rigtige id, den rigtige version, den
rigtige adresse — og to knapper. Værdierne står der frem for ordet
"anonymt", fordi et løfte om telemetri er præcis så meget værd som
læserens mulighed for at efterprøve det.

`keep` er en peger og ikke en bool. En tom krop måtte ikke kunne betyde
"slå det fra": et fravalg, ingen har truffet, er lige så meget et brudt
løfte som et tilvalg, ingen har truffet.

Og tre ting mere, som Yggdrasil har lært på den hårde måde:

Et beacon, der ikke kan nå sin collector, fejlede i fuldstændig tavshed.
Det prøvede igen i morgen, siden sagde "sidst sendt" med en dato, der
bare blev ældre, og ingen opdagede det. Grunden bliver nu skrevet ned og
vist.

En installation, der opdaterer sig selv om natten, meldte gårsdagens
version i op til et døgn, fordi døgnet blev talt fra sidste vellykkede
melding. En version, der er skiftet, sender nu med det samme.

Og det offentliggjorte tal har fået et gulv. Et tal, der falder, kan ikke
offentliggøres som en kendsgerning — en dårlig udrulning, en
DNS-ændring, der stille stopper meldingerne, og rækker, der er væk, ser
alle sammen ud som "færre bruger det nu".

Adressen bliver stående: Verdande melder til verdande.yggdrasilpanel.com,
ikke til panelets egen collector. De to tæller hver sin slags
installation, og en fælles tabel har intet felt til at holde dem ude fra
hinanden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 14:39:14 +02:00
Kristian Wind 445d9de57a Merge pull request #66 from kristianwind/indbakken-har-en-adresse-apps-kan-skubbe-til
CI / OpenAPI is valid (push) Successful in 11s
CI / End-to-end (push) Successful in 11m3s
CI / Docker image builds (push) Successful in 2m17s
Release / Build and publish image (push) Failing after 9s
CI / Go (push) Failing after 15m6s
feat(indbakke): en adresse, andre programmer kan skubbe en opgave til
v0.43.0
2026-09-08 14:12:17 +02:00
Kristian Wind d5ad459eba Merge pull request #65 from kristianwind/maerket-paa-ikonet
feat(beskeder): appens ikon bærer tallet på ulæste
2026-09-08 14:04:12 +02:00
Kristian Wind c297d256e4 Merge pull request #64 from kristianwind/tag-en-person-i-en-note
feat(noter): nævn en person i en note, og den er delt med dem
2026-09-08 14:04:08 +02:00
kristianwindandClaude Opus 5 d063425cae feat(indbakke): en adresse, andre programmer kan skubbe en opgave til
Der var to veje ind i forvejen, og ingen af dem passer til "gem den her,
mens jeg står med telefonen i hånden". API-tokenet giver adgang til alt
og skal have en JSON-krop skrevet rigtigt; mailadressen virker først, når
nogen har sat en mailserver op, og går gennem en indbakke, der kan være
minutter om det. Det, der manglede, var en URL, man kan smide en linje
tekst efter — fra en genvej på telefonen, et script, en tjeneste, der kan
kalde en URL.

Kroppen læses på tre måder, fordi de tre er dem, man møder: JSON fra en
tjeneste, et formularfelt fra en webhook-formular, og ren tekst fra en
genvej, der bare sender det, man har markeret. Første linje bliver til
opgaven, resten til noten under den. Linjen læses med quick-add-parseren,
så "Ring til Anders i morgen p1 #Firma" betyder det samme her som i
feltet øverst i fladen — dét er hele pointen med at have én parser: den
vej, teksten kom ind ad, ændrer ikke, hvad den betyder.

Sit eget token frem for at låne mailens. De to er den samme slags
hemmelighed, men de slipper ud hver for sig: en URL, der har ligget i en
genvej på en telefon, der blev væk, skal kunne skiftes uden at
mailadressen — som står i andres adressebøger — skifter med.

Og ikke et API-token. Et token herfra kan én ting: lave en opgave i den
persons indbakke. Det er hele forskellen på en hemmelighed, man kan lægge
i et script på en anden maskine, og en, man ikke kan.

Kun POST. En GET ville være nemmere at kalde, og det er præcis
problemet: forudindlæsning i en browser, en linkkontrol i en chat og en
historik, der bliver besøgt igen, ville alle lave opgaver, ingen har
bedt om. Sin egen kvote frem for postens, så en travl integration ikke
kan lukke for den indgående post.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 14:03:32 +02:00
Kristian Wind 0764fbd566 Merge pull request #63 from kristianwind/del-note-med-en-uden-konto
feat(noter): del en note med en, der ikke har en konto endnu
2026-09-08 12:13:34 +02:00
kristianwindandClaude Opus 5 b041b32aa2 feat(beskeder): appens ikon bærer tallet på ulæste
Klokken i hovedet siger, hvad der er nyt — men kun til den, der kigger på
fanen. Det halve af pointen med at få besked er at få den, mens man laver
noget andet, og det eneste sted, appen kan sige noget dér, er på sit eget
ikon: mærket i Dock'en på en Mac, på proceslinjen på en pc.

Sat to steder, fordi der er to tilstande. Appen selv, mens den er åben og
kender tallet, og service-workeren, når den ikke er — og det er netop
dér, mærket er det, der siger, at der er sket noget.

Tallet kommer med i push-beskeden frem for at blive talt op af
service-workeren. Den husker ingenting mellem to opvågninger og kunne
derfor kun lægge én til noget, den ikke ved hvad er: et mærke, der tæller
sig selv op og aldrig kommer ned igen. Serveren ved det i forvejen, og en
tælling, der fejler, udelader feltet frem for at sende et nul — så står
mærket, som det stod, i stedet for at blive tørret af en optælling, der
ikke lykkedes.

Ét tal, aldrig en optælling: mærket sættes til hvad der *er*, så det går
ned af sig selv, når beskederne bliver læst. Og det ryddes ved log ud —
det står i Dock'en, også når ingen er logget ind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 12:12:54 +02:00
kristianwindandClaude Opus 5 d8f5f6e236 feat(noter): nævn en person i en note, og den er delt med dem
At skrive nogens navn i en note *er* at ville have dem til at læse den.
Indtil nu var det to handlinger — skriv navnet, åbn panelet, find
personen, del — og den anden blev glemt, så noten stod med en besked til
en, der ikke kunne se den.

Nu deler `@Navn` noten med den, der bliver nævnt, og siger det til dem.
Editoren foreslår personer efter `@`, ligesom `#` foreslår projekter.

Slået op i navnene frem for tolket ud af teksten. Et navn kan have
mellemrum i sig, og et udtryk, der skulle fange dem alle, ville også
fange sætningen bagefter: `@Sofie i morgen` ville blive til en person,
der hed noget i den retning. Her er der ikke noget at gætte — der står et
navn, eller der gør ikke. Det er også dét, der gør det forsvarligt at
dele på: adgang må ikke afhænge af, hvor langt et regulært udtryk valgte
at læse. En mailadresse er ikke en omtale, det længste navn vinder, og
ejefaldets s hører til sætningen.

Kun nye omtaler, sammenlignet med teksten før gemningen på samme måde som
links: en note gemmes ved hver tastepause, så uden sammenligningen var
hver gemning en ny deling og en ny besked om det samme navn.

Kun ejerens omtaler deler. En medredaktør må skrive i noten, men ikke
give den videre — ellers kunne den, ejeren har delt med, skrive et navn
og dermed dele ejerens note med en, ejeren aldrig har hørt om. Det er
den samme regel, panelet følger.

Læser, ikke medredaktør: en omtale siger "se på det her", og det mindste,
der opfylder sætningen, er det rigtige at give. En rolle, ejeren har
hævet i panelet, sænkes ikke igen af en omtale.

Og én besked pr. handling: den, der lige er blevet nævnt, får "nævnte
dig" og ikke også "rettede en note" om den samme gemning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 12:08:12 +02:00
Kristian Wind 61634a6bb5 Merge pull request #62 from kristianwind/noter-og-opgaver-giver-besked
feat(beskeder): klokken siger til, når nogen rører noget, der er dit
2026-09-08 11:59:04 +02:00
kristianwindandClaude Opus 5 8ee9fbedfb feat(noter): del en note med en, der ikke har en konto endnu
En note kunne deles med dem, der allerede var på instansen — og kun med
dem. Skulle en anden læse med, skulle man først invitere dem ind, vente
på at de oprettede sig, finde noten frem og så dele den. Fire skridt og
en ventetid for det, der er én sætning: lad Sofie se den her.

Nu er der ét felt. Man skriver adressen, og serveren ved, om der er en
konto bag: er der det, bliver det en deling med det samme, og ellers en
invitation, der bliver til delingen i samme øjeblik, kontoen oprettes.
Hvem der har en konto, er ikke noget, man skal vide for at dele en note.

Rækken er den, der allerede fandtes. En invitation bar i forvejen en
e-mail, en rolle og et forseglet token; det eneste, der manglede, var at
kunne sige, hvad den var en invitation *til*. note_id ved siden af
project_id, med fremmednøglen — dén, der rydder invitationen væk, når
noten slettes, inden nogen når at tage imod en invitation til noget, der
ikke er der længere.

Det er ikke ny magt til nogen: enhver, der ejer et projekt, kunne i
forvejen invitere en fremmed ind på instansen pr. e-mail. Kun ejeren kan
invitere til en note, som kun ejeren kan dele den.

De inviterede står i panelet ved siden af dem, der er kommet, og kan
trækkes tilbage. Uden det er en invitation sendt i går usynlig, og så
sender man den igen — endnu et link til den samme indbakke, og kun det
ene af dem til at gøre ugyldigt.

Og delingen siger selv til: den, der får en note delt, får besked om, at
den er der. Klokken sorterer nu på id efter tid, fordi tiden er sekunder
— en note, der bliver delt og rettet i samme håndbevægelse, stod ellers
i den rækkefølge, basen nu engang fandt dem i.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 11:58:47 +02:00
kristianwindandClaude Opus 5 ab77024ec9 feat(beskeder): klokken siger til, når nogen rører noget, der er dit
En delt note blev rettet af en anden, og man opdagede det kun ved at åbne
den. En opgave blev givet til en, og den lå bare der. Det, andre gør ved
det, man deler, skal man kunne se uden at lede efter det.

Klokken i hovedet: ulæste beskeder om noter, der er blevet rettet, delt
med en, eller opgaver, man har fået. Sætningen skrives i browseren, ikke
på serveren, så et engelsk interface får en engelsk klokke.

Beskeder om den samme note bliver slået sammen, mens de er ulæste. En
note gemmes på en tastepause, så en række pr. gem ville lave én person,
der skriver i fem minutter, om til tyve ens linjer. Er beskeden læst, er
den næste rettelse en ny ting at få at vide — ikke en gentagelse.

note_id får sin egen søjle ved siden af project_id og task_id frem for et
generisk (kind, id)-par: de tre er ikke det samme slags nul, og
fremmednøglen — dén, der rydder beskeden op, når noten slettes — kan et
generisk par ikke have.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 11:40:43 +02:00
Kristian Wind 48509e3073 Merge pull request #61 from kristianwind/delte-noter-tager-links-med
Release / Build and publish image (push) Failing after 8s
feat(noter): en delt note tager de noter med, den peger på
v0.42.15
2026-09-07 22:59:47 +02:00
kristianwindandClaude Opus 5 916a0c0224 feat(noter): en delt note tager de noter med, den peger på
Den anden halvdel af den melding, der begyndte det her: linkene i en delt note
førte ingen steder hen, fordi det, de pegede på, ikke var delt. At dele en note
er at dele det, den *siger*, og et link er en del af det.

note_shares får en `linked`-søjle. 0 er en deling, nogen har valgt; 1 er en, der
er udledt af et link. Skellet er hele modellen. Fladen kan sige *hvorfor* en
person har en note — "fulgte med" er en anden sætning end "du delte den" — og de
udledte rækker kan regnes forfra efter hver ændring, uden at røre dem, ejeren
selv har sat.

SyncLinkedShares regner billedet om fra bunden efter enhver af ejerens egne
handlinger: en deling, en fjernet deling, en gemning der har ændret links eller
titel, en sletning. Hele kæden følger med, ikke kun første led, med et loft på
hundrede noter — "del denne ene note" må ikke kunne blive til fire hundrede.
Rollen arves fra den note, man kom fra, og stærkest vinder, når en note kan nås
ad to veje.

Kun ejerens egne noter følger med, og det er ikke en optimering: et link peger
på en titel, og en medredaktør må skrive titler. Kunne et link hente hvad som
helst, kunne den, man har delt med, skrive [[Bankoplysninger]] og læse med.
Derfor slås der kun op blandt ejerens egne noter, og derfor regner kun ejerens
egne gemninger om. Begge dele har en Go-prøve.

Det lukker ikke hullet helt: skriver medredaktøren linket, og gemmer ejeren
bagefter sin egen note, følger den note med dér. Det, der står imod, er
synlighed — linket står som tekst i ejerens egen note, og panelet viser hver
note, der er fulgt med, og hvem der har den.

Udregningen åbner hver af ejerens noter, fordi titlerne er forseglet. Derfor to
værn: har man ikke delt noget, svares der med det samme ud fra ét EXISTS, og en
gemning regner kun om, når links eller titel faktisk har flyttet sig. Uden det
første kostede "Ny note" halvfems millisekunder, og i det hul nåede e2e-prøven
for monospace at taste sin titel ind i den forrige note, som editoren så tegnede
væk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013r4DxpUZcSShxfCZZaeMGZ
2026-09-07 22:28:58 +02:00
Kristian Wind ff75302cf8 Merge pull request #60 from kristianwind/note-links-og-kodeknap
fix(noter): følg note-links i en delt note, og lad </> være kode
2026-09-07 21:58:29 +02:00
kristianwindandClaude Opus 5 08d703e3af fix(noter): følg note-links i en delt note, og lad </> være kode
To ting meldt af en, der havde en note delt med sig.

Note-linkene i den delte note gjorde ingenting. Opslaget faldt tilbage på det
første søgeresultat, når ingen titel passede — og i en delt note er det første
resultat næsten altid noten selv, fordi den nævner den titel, den peger på. Så
åbnede linket den note, man allerede stod i. Kun en titel, der passer helt,
tæller nu; og rammer den forbi, siger beskeden den rigtige grund: en note, der
deles, deler ikke de noter, den peger på, så modtageren kan se linket uden at
kunne åbne det.

Og `</>` sad på kildevisningen med teksten "Vis som kode" på. Tegnet betyder
inline kode i ethvert andet program, så et markeret ord plus et tryk gav hele
noten som Markdown. Nu gør `</>` det, det siger — ⌘E, en kontakt, og ordet under
markøren når der ikke er markeret noget — og kilden har fået Markdown-mærket og
sin egen tekst.

Kode indsættes som en node frem for med execCommand: Chromium renser den HTML
gennem indsætningsvejen og gav et <span> med skrifttypen skrevet ind i sig, som
htmlToMarkdown ikke kan skrive ned som kode.

To e2e-prøver, som begge fejler på koden før den her.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013r4DxpUZcSShxfCZZaeMGZ
2026-09-07 21:50:12 +02:00
Kristian Wind d4784fd174 Merge pull request #59 from kristianwind/upcoming-horizon
CI / OpenAPI is valid (push) Successful in 12s
CI / End-to-end (push) Successful in 9m37s
CI / Docker image builds (push) Successful in 2m9s
Release / Build and publish image (push) Failing after 8s
CI / Go (push) Failing after 14m45s
Kommende: en horisont på 7, 14 eller 30 dage
v0.42.14
2026-09-07 15:08:06 +02:00
kristianwindandClaude Opus 5 094ea5cdd8 feat(upcoming): look up to 30 days ahead, and choose how far
Kommendes liste has always been the next seven days and nothing else. Seven days
is the right walk to take in the morning, and it is still what the view opens on
— but "when does this month get busy" is a different question, and the month grid
answers it with shape rather than in order. So the list now takes a horizon: 7,
14 or 30 days, chosen with a switcher beside the view buttons.

Only the list gets one. A week grid is a week and a month grid is a month; there
is no fortnight for them to show, so the switcher is hidden in those modes.

The choice lives in localStorage next to the mode, under its own key. It is the
same kind of choice as list-or-grid — how you want to look, not a fact about the
work — and it should survive a reload without following you to a phone where
thirty day headings is a very long scroll.

Once the strip is longer than a week the day headings carry the week number.
Thirty days repeat every weekday four times, and "mandag 6. okt" alone does not
say which mandag.

Server side the endpoint already took `?days=` and capped it at 31, so the window
needed nothing — but its row limit was a constant 1000 written for a week. A
month asked for with a week's ceiling would truncate the last days silently, and
a day cut off the end of the answer looks exactly like a day with nothing due, so
the limit now follows the window.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SA1TDmgQZw3AufJqkbwezx
2026-09-07 14:46:49 +02:00
Kristian Wind f60758b770 Merge pull request #58 from kristianwind/site-all-themes
docs(site): show all six themes, and put the demo line in English
2026-09-04 09:56:28 +02:00
kristianwindandClaude Opus 4.8 9d390d021f docs(site): show all six themes, and put the demo line in English
Adds a gallery of the same Today view in each of the six themes — Northern lights,
Charcoal, Dusk, Daylight, Paper, Contrast — so the range is shown rather than
described, on the landing page and (as the swatch panel) the docs home. The lone
Charcoal Today capture is folded into it.

And the quick-add demo that leads the docs — "betal moms i morgen kl 10 p1 #Firma
@regnskab" — is now English, "file the VAT return tomorrow at 10am p1 #Accounts
@tax", on every page it appears as the line to type: the home, the quick-add page,
the API and MCP examples, and the OpenAPI blurb. The Danish date words on the
quick-add page stay, because documenting the Danish parser is their job.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-04 09:46:50 +02:00
Kristian Wind 18db79c063 Merge pull request #57 from kristianwind/site-charcoal-features
docs(site): Charcoal theme, offline, and the star that comes off
2026-09-04 09:27:50 +02:00
kristianwindandClaude Opus 4.8 1612f53966 docs(site): the Charcoal theme, offline, and the star that comes off
Refreshes the whole screenshot set to the app as it ships now — the three-line
note rows, the lightened calendar — and adds two Charcoal captures, the theme
modelled on Apple Notes, since it is the one worth showing. The landing page and
the docs home both gain it, a "holds through a short network drop" line, and the
correction that a starred or flagged mail now loses its mark once it is a task
rather than keeping it. Six themes now, not five.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-04 09:20:11 +02:00
Kristian Wind 4d5f1a56b1 Merge pull request #56 from kristianwind/gmail-unstar
CI / OpenAPI is valid (push) Successful in 2m55s
CI / End-to-end (push) Successful in 8m3s
CI / Docker image builds (push) Successful in 1m46s
Release / Build and publish image (push) Failing after 7s
CI / Go (push) Failing after 13m23s
feat(gmail): take the star off a mail once it has become a task
v0.42.13
2026-09-04 09:06:31 +02:00
Kristian Wind 274e392708 Merge pull request #55 from kristianwind/offline-cache
feat(offline): hold through a short network drop with a caching worker
2026-09-04 08:57:38 +02:00
kristianwindandClaude Opus 4.8 84ca506961 feat(gmail): take the star off a mail once it has become a task
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 <noreply@anthropic.com>
2026-09-04 08:57:07 +02:00
kristianwindandClaude Opus 4.8 6db61b0cc7 test(e2e): block the service worker except where the test is about it
The caching worker controls every page once it registers, and Playwright's
page.route() does not see the fetches a worker makes — so the failed-load test,
which forces a 500 to check the error path, got the real 200 through the worker
and read as a broken feature. Block the worker by default and turn it back on only
for the offline test, which is the one that needs a real one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-04 08:12:15 +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
Kristian Wind 7770ba9844 Merge pull request #54 from kristianwind/tasks-first-tap-rounded
CI / OpenAPI is valid (push) Successful in 14s
CI / End-to-end (push) Successful in 11m2s
CI / Docker image builds (push) Successful in 2m40s
Release / Build and publish image (push) Failing after 8s
CI / Go (push) Failing after 15m27s
Rows: open on the first tap on iPhone, round the hover highlight
v0.42.12
2026-09-03 22:23:08 +02:00
kristianwindandClaude Opus 4.8 55b8e438ad fix(rows): open on the first tap on iPhone, and round the hover highlight
On a touch screen a :hover on the tap target makes iOS spend the first tap
applying the hover and only open on the second — the double-tap Kristian hit
on tasks (and notes). The row hover is gated behind @media (hover: hover)
now, so a touch has no hover to apply and the first tap opens. The note
row's action buttons, which the hover used to reveal, are shown outright on
touch instead. And the task-row hover highlight is a rounded box (border
radius on the row), the same corner as a note in the list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-03 22:15:23 +02:00
Kristian Wind 1607a22f0c Merge pull request #53 from kristianwind/charcoal-separators
CI / OpenAPI is valid (push) Successful in 11s
CI / End-to-end (push) Successful in 9m56s
CI / Docker image builds (push) Successful in 2m2s
Release / Build and publish image (push) Failing after 5s
CI / Go (push) Failing after 15m10s
Charcoal: lighter day separators in the note list
v0.42.11
2026-09-03 20:54:56 +02:00
kristianwindandClaude Opus 4.8 8a5dfef41a fix(theme): lighter day separators in Charcoal
In Charcoal the note-list day separators used --ground, the darkest colour
in the theme, so they read as dark bars on the lighter list. They take the
menu's own resting grey (#2d3133) now — the colour "Projekter" sits on —
via a --note-group-bg fallback token, so every other theme keeps --ground.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-03 20:46:51 +02:00