Meldt ind: "command+b viser og skjuler menuen - ikke bold".
Fladen tog ⌘B til sidebjælken, og begrundelsen stod i koden: "works while
typing as well — it moves nothing in the field". Det passer for et
`<input>`, hvor ⌘B ikke gør noget. I en contenteditable ER ⌘B fed tekst,
og `preventDefault()` stoppede browseren i at gøre den.
Sætningen var sand, da den blev skrevet. Så kom en rig editor til, og
begrundelsen holdt op med at passe uden at nogen rørte linjen — samme form
som flere af fælderne i den her kodebase.
Feltet, markøren står i, har førsteret til en tast, det selv bruger. Uden
for et skrivefelt folder genvejen stadig sidebjælken, og prøven måler
begge halvdele: en rettelse, der bare holdt op med at folde, ville være
halvt færdig.
Efterprøvet mod den gamle kode — ordet blev aldrig fedt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
To ønsker, meldt sammen.
--- eksport af det hele ---
Eksporten fandtes allerede — konto som JSON, noter som Markdown i en zip,
og pr. projekt CSV i Todoist-format og ICS. Hullet var, at opgaver kun
kunne hentes ÉT PROJEKT AD GANGEN. Med femten projekter er det femten
downloads for at flytte sine egne opgaver, hvilket er en eksport, der
teknisk set findes, og som ingen bruger.
`GET /export/projects.zip` er én CSV pr. projekt. En zip og ikke én bred
fil med en projektkolonne: Todoists egen eksport er en fil pr. projekt, så
det er den form, der importeres i den anden ende — og den form, man kan
hive ét projekt ud af uden at åbne et regneark. Samme begrundelse som
noteeksporten, der er en mappe med Markdown og ikke én lang fil.
`GET /export/tasks.ics` er alle daterede opgaver i én kalender. Ét fil og
ikke en zip, fordi en kalender er lavet til at rumme mange ting, og
projektet følger med hver opgave. Udaterede udelades — en VTODO uden dato
er en linje, de fleste læsere taber på gulvet.
Prøven ser I filerne frem for på statuskoden: en tom zip og en kalender
uden VTODO'er er begge 200 med en troværdig Content-Type, og det er
præcis den form, en fejl i en eksport har. To projekter, ikke ét, så en
løkke der kun eksporterer det sidste ikke består.
--- en indsat liste bliver til opgaver ---
Quick-add er ét felt på én linje, og browseren smider linjeskiftene væk
ved indsætning i et `<input>` — så en kopieret liste blev til én opgave
med punkterne mast sammen.
Nu læses den. Indrykning bærer niveauet, og bredden læses af teksten frem
for at være gættet til to eller fire: en liste fra Apple Noter rykker ind
med ét antal mellemrum, en fra et Markdown-dokument med et andet, og
begge skal give ét niveau. `- `, `*`, `1.` og `1)` fjernes som den
notation, de er. `- [x]` er derimod indhold: en halvt afkrydset liste,
der kommer ind som lutter uløste opgaver, har tabt det, man indsatte den
for.
Teksten røres ikke ud over mærket. Datoer, `#projekt` og `@etiket` læses
af quickadd på serveren, præcis som når linjen var skrevet i hånden — ét
sted at forstå en opgavelinje, ikke to. Derfor tager `/tasks/quick-add`
nu et `parent_id`: en indrykket linje skal parses som enhver anden.
En undertask lever i sin forælders projekt. Et `#projekt` på en indrykket
linje honoreres derfor ikke, og det siges højt som `unknown_project` frem
for at ordet bare forsvinder fra titlen.
Indsætter man i en opgaves undertask-felt, bliver linjerne undertasks
dér. Det er "enten opgaver eller undertasks" afgjort af hvor man
indsætter, uden at nogen skal vælge noget i en menu.
Listen vises, før den oprettes. Fyrre opgaver fra et fejlklik er ikke
noget, man skal opdage bagefter — og visningen er også svaret på "forstod
den indrykningen", stillet mens der stadig er en Fortryd-knap.
--- rettet undervejs ---
Rækkens undertask-tæller stod stille, når en undertask blev tilføjet fra
ruden: rækken sagde 1/2, mens ruden foran viste fire. Tallene bor på
forælderen og regnes ud ved hentning, og serveren sender ikke forælderen
igen, fordi et barn blev lavet. Det gjaldt også den gamle vej, ikke kun
den nye.
Go-suiten grøn. Fuld chromium-suite: 103 bestået, 0 fejlet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Meldt ind: "kun den øverste sætning af det jeg markerede blev lavet om til
monotype". Det førte til en anden fejl, der var værre.
--- den meldte ---
Monotype går uden om execCommand, fordi formatBlock pakker et <p> ind i
<pre>'en i stedet for at lave den om. Den vej spurgte efter
`currentBlock()`, og det er ét blok — markørens. Så af en markering over ti
linjers terminaludskrift blev den første linje til kode og resten stod
tilbage som brødtekst.
`selectedBlocks()` læser hele markeringen, og de markerede blokke lægges
sammen til ÉN blok. Ikke én pr. linje: en udskrift, man indsætter, er ét
stykke kode, og tre blokke ved siden af hinanden ser næsten ens ud på
skærmen og bliver til tre hegn i filen.
Vejen ud af monotype tager nu også hele markeringen. `swapTag()` bytter
tagget uden at røre markøren — tekstknuderne flytter med over, så den range
browseren holder på stadig peger på noget i dokumentet, og formatBlock kan
klare resten bagefter.
--- den anden, som prøven fandt ---
`watchForTrouble` meldte "ReferenceError: codeText is not defined" fra et
rekursivt kald, jeg havde glemt at omdøbe. Ved siden af det lå en KOPI af
"hvad er teksten i en blok", inde i editoren, med præcis den <br>-fejl, der
blev rettet i #91: `textContent` på et <br> er den tomme streng.
Den kopi bruges af farvelægningen, som maler resultatet TILBAGE ind i
blokken. Så en kodeblok skrevet med shift+retur fik sine linjer skrevet
sammen i det øjeblik, man klikkede væk fra den — på skærmen, ikke kun i
filen, og uden at nogen havde rørt teksten. Målt mod den gamle kode:
"echo enecho to".
Kommentaren over kopien sagde ordret, at de to læsninger skulle være ét
sted, FORDI en uenighed ville ende i noten. Den var sand, da den blev
skrevet; kopien kom senere. `blockText` i richtext.js er nu det ene sted,
eksporteret og brugt begge steder.
Begge prøver er efterprøvet mod den gamle kode.
Fuld chromium-suite kørt to gange: 94 bestået, 0 fejlet, begge gange.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tre ting meldt ind sammen. De to første manglede; den tredje var i stykker.
--- man kunne søge og ikke vælge ---
Regression fra #84. Siden holdt op med at SKRIVE adressen, men blev ved med
at læse den ved hver ændring:
let asked = $derived($page.url.searchParams.get('note'));
$effect(() => { if (!asked || selectedId === asked) return; … });
Kom man fra ⌘K med `?note=<id1>`, stod `asked` fast på id1 for altid.
Hvert klik på en anden række satte `selectedId`, effekten så uenigheden og
åbnede id1 igen. Man blev trukket tilbage til den første træffer.
En adresse, der ikke følger med, er ikke en tilstand at holde sig enig med.
Den læses nu ved ankomsten — og der er TO ankomster, hvilket den første
udgave af rettelsen ikke tog højde for: layoutet tegner ingenting, mens
kontoen hentes, så siden monteres efter den første navigation er forbi, og
en lytter registreret der hører den aldrig. `/noter?note=<id>` tastet eller
fulgt udefra åbnede så ingenting. `onMount` og `afterNavigate`, samme
funktion, med `id === selectedId` som værn mod at begge fyrer.
--- en blok kunne ikke gemme flere linjer ---
Shift+retur bliver i blokken, men det browseren lægger ind er et <br>, og
`textContent` på et <br> er den tomme streng. Blokken blev skrevet til
Markdown som én linje: "first linesecond line". Usynligt indtil en
genindlæsning, for på skærmen står linjerne rigtigt hele tiden.
`codeText()` læser blokken rekursivt og gør <br> til et linjeskift.
Rekursivt, fordi indholdet ligger i et <code> inde i <pre>: en filtrering
over <pre>'ens egne børn spørger det ene element om en textContent, der
allerede har tabt linjeskiftet.
--- lister havde ingen vej til niveau to ---
Tab var ikke bundet til noget uden for forslagslisten, så den gjorde det,
Tab gør: flyttede fokus ud af noten. Den rykker nu ind og shift+Tab ud, og
kun inde i en liste — ellers er noten en boks, man ikke kan tabbe ud af.
Og skriveren ville have tabt resultatet: `list()` læste kun en underliste,
der ligger INDE i sit punkt. Lægger browseren den som søskende, og de gør
det uens, blev grenen sprunget over uden lyd. At binde Tab uden den her
halvdel ville have lavet en fejl, hvor indrykkede punkter forsvandt ved
gemningen.
Begge nye prøver måler EFTER en genindlæsning. Før den ser begge dele
rigtige ud på skærmen; det er turen gennem Markdown, der afgør, om det
blev skrevet ned.
Fuld chromium-suite kørt to gange: 92 bestået, 0 fejlet, begge gange.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Panelet hang under klokken med `right: 0` og en fast bredde på 340 px.
Klokken sidder ikke yderst — der er en temaknap, et mellemrum og en
kantafstand til højre for den — så der er kun omkring 330 px tilbage, og
resten blev skubbet ud over venstre kant og klippet af. Målt til -10 px ved
en skærmbredde på 390; på en smallere telefon mere. Overskriften "Beskeder"
klæbede til kanten uden luft, hvilket er det synlige tegn.
På en telefon hører panelet nu til skærmen i stedet for til klokken: det
spænder fra kant til kant med en margen i hver side, under toplinjen. Samme
knæk ved 820 px som toplinjens egen menuknap, så de to skifter form
samtidig. Det får også `overscroll-behavior: contain`, så det at rulle i
listen ikke trækker siden bagved med.
Toplinjens underkant måles frem for at blive regnet ud. Bredden KUNNE
trækkes fra i CSS, men så ville den her fil kende sine naboers størrelse, og
den næste knap i toplinjen ville lave fejlen om igen uden at sige noget.
--- og fire ustabile prøver mere ---
Fladen tegner hver ændring med det samme og sender kaldet bagefter. Alt, man
kan se, er altså sandt, før serveren har hørt om det, og en prøve, der
genindlæser eller klikker videre i det hul, måler den gamle tilstand.
`en monospace-linje kan forlades` ventede 1200 ms før en genindlæsning.
Datoprøven ventede 400 ms. Et gæt om, hvor hurtig maskinen er, holder indtil
CI er travl — begge fejlede i en fuld kørsel her.
`en gruppe er en side med sine projekter` er den mest lærerige: trækket
virkede, og `toPass` blev grøn på den lokale tilstand, hvorefter klikket
videre hentede en gruppe uden projekter i. Prøvens egen kommentar gætter på
et upålideligt HTML5-træk; det var gemningen, der ikke var nået frem.
Monospace-prøven havde OVENI en anden fejl, som først viste sig, da den
første var lukket: den klikkede "Ny note" og skrev med det samme. `create()`
venter på serverens svar, og imens står den forrige notes editor stadig på
skærmen, så teksten gik ind i den forrige note og den nye blev tom. Ny
hjælper `nyNote()` venter på et tomt felt. Omkring 14 andre steder i filen
har samme mønster; de er ikke skrevet om, fordi de består, og en samlet
omskrivning ville bytte kendte prøver for ukendte.
Det hul findes også for et menneske: klikker man Ny note og skriver straks,
lander de første anslag i den forrige note. Det er millisekunder lokalt og
sekunder på en dårlig forbindelse. Samme familie som #86, men tiden FØR
svaret frem for efter. Noteret som et selvstændigt punkt, ikke rettet her.
`noteGemt()` fra #88 er nu `gemt(side, sti)` med noteGemt, opgaveGemt og
projektGemt ovenpå. Der står 23 faste ventetider tilbage, men ingen af dem
lige før en genindlæsning eller en navigation.
Fuld chromium-suite kørt to gange: 90 bestået, 0 fejlet, begge gange.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Samme form som de fire "Gemt"-ventetider, to andre steder — begge lige før
en `page.reload()`, hvilket er der, hvor det koster.
`en note kan skrives fed, og den kommer hel tilbage` havde `blur()` direkte
efterfulgt af en genindlæsning uden noget værn overhovedet. Nåede PATCH'en
ikke frem først, målte prøven en note, der aldrig blev skrevet — og den
prøve findes netop for at fange en omformer, der er uenig med sig selv.
`[[henvisning]]`-prøven havde `waitForTimeout(900)`. Tallet var gættet til
at overleve tastepausen på 700 ms; et gæt om tid måler maskinen frem for
programmet. Det er den konstruktion, den her fil allerede advarer imod.
Begge venter nu på PATCH-svaret gennem noteGemt().
Der står 24 faste ventetider tilbage i filen. De er ikke rørt: flere af dem
venter med vilje (at skrive i et menneskeligt tempo er en rigtig grund), og
at skrive dem om samlet ville bytte kendte prøver for ukendte.
Fuld chromium-suite: 89 bestået, 0 fejlet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fire steder ventede prøverne på teksten "Gemt" i notefoden. Den er
hvilestanden: feltet siger "Gemmer" mens en gemning kører og "Gemt"
ellers — altså også før den første gemning overhovedet er begyndt. Alle
fire ventetider var grønne med det samme og ventede på ingenting.
To af dem dækkede over et rigtigt kapløb:
- `en delt note tager de noter med, den peger på` skriver i en note og
navigerer væk umiddelbart efter. Nåede PATCH'en ikke frem først, var
teksten væk, da siden kom igen.
- `en gruppe er en side med sine projekter` har samme form et andet sted:
beskrivelsen tegnes optimistisk — siden sætter teksten på gruppen straks
og sender PATCH'en bagefter — så kontrollen var grøn, før serveren havde
hørt om den, og `page.reload()` lige efter hentede den gamle gruppe
tilbage. Prøven fejlede altså på noget, der ikke var i stykker.
De to sidste ventede på ingenting uden at skade noget, men var stadig
vildledende at læse.
Der er nu én hjælper, `noteGemt(side)`, der venter på selve PATCH-svaret,
med forklaringen på hvorfor "Gemt" ikke duer skrevet ét sted.
Gruppeprøven venter tilsvarende på sin egen PATCH. Hvor rækken i listen
allerede var det rigtige værn, er den overflødige linje væk.
Samme form som reglen i CLAUDE.md om at læse, hvad en kontrol skrev, da
den BESTOD: "venter på gemning" og "matchede den tekst, der altid står
der", er samme grønne.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Opstarten spørger serveren, hvem man er. Når svaret aldrig kommer, står
`user` som null — og null betyder "logget ud" alle andre steder i
programmet. Så et totalt udfald blev vist som login-skærmen: appen gav
sessionen skylden for, at nettet var nede, og tilbød et kodeordsfelt, der
umuligt kunne virke.
Det er værre end en fejlbesked, fordi det ligner noget, der kunne virke.
Man taster kodeordet, der sker ingenting, og man konkluderer, at kontoen
er i stykker. Det skete i virkeligheden 17. september 2026, da instansens
DNS-post blev slettet: det, appen viste, var en langsom login-side.
Forskellen var der allerede i forespørgselslaget. Et netværkssvigt, der
har brugt sit forsøgsvindue op, kastes som `ApiError(0, 'offline')` —
koden laves af klienten selv, netop fordi der aldrig kom en status. En 401
er serveren, der svarer, og det er en rigtig udlogning. Butikken skelner nu
på den kode, og fladen viser NoConnection i stedet for SignIn.
Teksten siger, at data er i behold, for det er det spørgsmål, man faktisk
har, når ens noter forsvinder bag en login-boks.
Prøven afbryder API-kaldene frem for at stoppe serveren: en afbrudt
forespørgsel er præcis det, en manglende DNS-post giver browseren.
Efterprøvet mod den gamle kode — den fejler på "Ingen forbindelse".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Serveren tømmer `body` på listerækker med vilje, så en tom krop var sidens
tegn på "det her er en række, hent noten hel". En nyoprettet note er også
tom — og hel — så den blev sendt samme vej: editoren blev pillet af
skærmen, noten hentet igen, og editoren sat op på ny.
I det hul er der ingen valgt note, og save() går tavst tilbage uden en.
Skrev man der, forsvandt teksten sporløst: ingen fejl, ingen toast, bare en
tom note. Svaret på oprettelsen var hele noten hele tiden, så opslaget var
også overflødigt.
Det er samtidig grunden til, at søgeprøven var ustabil. Dens værn var en
ventetid på "Gemt", og den tekst er hvilestanden — feltet siger "Gemmer"
mens en gemning kører og "Gemt" ellers, altså også før den første gemning
er begyndt. Prøven fik grønt med det samme og målte ingenting; i CI var
maskinen langsom nok til at ramme hullet bagefter. Den venter nu på et tomt
felt før den skriver, og på rækken i listen bagefter.
Den nye prøve tæller opslag frem for at måle et blink: et blink ville kræve
en prøve, der er hurtigere end programmet, hvilket er præcis den fejl den
lukker. Efterprøvet mod den gamle kode — den fejler på
`expect(fetched).toEqual([])`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Plausible på vores egen maskine. Ingen cookie, ingen gemt adresse, ingen
tredjepart — og derfor ingen samtykkebjælke på en side, der skal sælge noget.
`defer`, så tælleren aldrig er det, der holder siden tilbage.
Det gælder siden, ikke programmet. En installation melder det, der står under
"It counts itself, out loud", og ikke mere; de to ting deler ikke noget.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fra en skærmoptagelse, to billeder i sekundet:
2,5 s feltet "verd" listen ufiltreret
3,0 s feltet "nde" listen filtreret
6,0 s feltet "verdande" listen ufiltreret
6,5 s feltet tomt listen filtreret
To fejl oven i hinanden, og den ene er min fra #79.
Bogstaverne forsvandt, fordi adressen blev skrevet ved hvert tastetryk, så en
søgning kunne gemmes og deles. `replaceState` opdaterer `$page.url` et øjeblik
efter, den kaldes, så to hurtige tastetryk gav en effekt, der læste den *forrige*
adresse, fandt den uenig med det skrevne, og skrev den gamle værdi tilbage i
feltet. `untrack` fjernede sløjfen mellem de to effekter; den fjernede ikke
kapløbet med browserens egen forsinkelse.
En markør, der skal holde to retninger enige, taber kapløbet et sted. Adressen
skrives derfor ikke længere af siden — kun af navigation — og der er én effekt
tilbage, som læser den. Prisen er, at et ryddet søgefelt ikke rydder `q` af
adressen. Det er en mindre løgn end at æde bogstaver.
Den anden fejl har ligget der hele tiden: `load()` havde ingen vagt mod forældede
svar. To hentninger kunne være undervejs på én gang, og den langsomste vandt — så
listen viste svaret på et spørgsmål, der ikke længere blev stillet. Pausen på
250 ms gjorde det sjældnere og kunne ikke gøre det umuligt; det stod endda i
kommentaren ved siden af. Hver hentning har nu et nummer, og kun det nyeste
skrives i listen.
Prøven skriver i et menneskeligt tempo, for det var hastigheden, der afslørede
det, og holder den ene af to hentninger tilbage for at tvinge rækkefølgen om.
Kørt mod den gamle kode fejler den med `Received: ""` — feltet var tømt helt.
Bemærket: "en gruppe er en side med sine projekter" fejlede to fulde kørsler i
træk og bestod på den tredje, og alene. Den er ikke rørt her. Sammen med "en delt
note tager de noter med" i går er der nu to prøver, der flakker.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rapporteret med et billede af en beskrivelse, der manglede sit første bogstav på
hver linje: "hermostat RU0910301440" og "atteries". Det er ikke manglende tegn —
det er et felt, der er rullet til højre. Og det ligner ikke rulning; det ligner,
at bogstaverne ikke er der.
To ting på én skærm.
Titlen var et <input>, og et input kan ikke ombryde. "tado°: Your Smart
Thermostat needs new batteries" er en almindelig opgavetitel, og på 390 px var
feltet 490 px bredt i en kasse på 356 — halvdelen af overskriften stod uden for
kanten. Den er nu et <textarea>, der vokser med sin tekst. Retur gemmer frem for
at lave en ny linje: en titel er én linje, uanset hvor mange den fylder på
skærmen. Trækkehåndtaget er væk, for højden sættes af teksten, og et håndtag
ville love noget, det næste bogstav retter tilbage.
Beskrivelsen er tit et klippet stykke mail med en URL i. Et <textarea> ombryder
ved mellemrum, og en URL har ingen.
`break-word` og ikke `anywhere`: den første bryder kun et ord, der ikke kan stå på
en linje alene, mens den anden bryder ved først givne lejlighed og delte
"GarageRisteriet" midt i en stavelse. Den lektion står allerede ved h1 på
projektsiden og er betalt én gang.
Prøven måler frem for at beskrive: et felt, der ruller vandret, har en
scrollWidth større end sin clientWidth. Kørt mod den gamle komponent fejler den
med "Opgavens tekst: 490 > 356".
Men kun den ene halvdel. Beskrivelsen løber ikke over i Chromium, heller ikke
uden reglen — det var Safari, den blev set i. Prøven måler derfor den regel frem
for dens virkning, og det står skrevet i prøven, så den næste ved, hvad den
vogter og hvad den ikke gør.
Bemærket undervejs: "en delt note tager de noter med, den peger på" fejlede én
gang ud af to fulde kørsler med "Delt med Mikkel." uden antallet. Den består
alene og i næste fulde kørsel. Den er ikke rørt her, men den flakker.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Den udrullede side på verdandetodo.com er fra 20.-22. august — den matcher en
commit fra før v0.42.15. Repoets egen kopi er fra 4. september. Begge stopper,
hvor "Lately" stopper, og den sagde 0.42.
Ni nye poster: billederne i noter, søgningen der bliver stående, deling af et
projekt ved navn, besked og udgang, søgning i indstillinger, dagens plan som
kort, browservalget, hele AI-laget, versionsbeskeden til administratorer, og
krogen der lader hvad som helst lægge en opgave i indbakken.
De to ældste ryger. Med ni nyere poster er 0.34 og 0.33 ikke "for nylig" længere,
og hele historien står på udgivelsessiden, som afsnittet selv henviser til.
Siden udrulles i hånden — der er ingen arbejdsgang, der gør det. Det er derfor,
den er tre en halv uge bagud, og det er værd at tage stilling til separat.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Prøven fra i går fejlede i CI på en PR, der kun rørte dokumentation — altså
flakkede den, og det er min.
Løkken laver tre noter og klikkede "Ny note" igen, uden at vente på at den
forrige gemning var kvitteret. Her på maskinen nåede den altid med; på en
belastet runner gjorde den ikke, og listen manglede noten. Fem sekunders
tålmodighed hjalp ikke, for det var ikke ventetid, der manglede — det var et
signal.
Der ventes nu på "Gemt". Kørt tre gange i træk grønt.
En prøve, der er hurtigere end programmet, måler den maskine, den kører på.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hjemmesiden har vist en Verdande fra før v0.43. /links/, /beacon/ og
/notifications/ svarede alle 404, måneder efter de blev skrevet.
Årsagen: docs-jobbet byggede på hvert push og udgav kun på en manuel udløsning,
med en note om at det ville køre, "once Pages is enabled". Pages BLEV slået til
— og betingelsen blev aldrig rettet. Siden er dermed bygget fyrre gange og
udgivet nul, uden at noget fejlede, for at bygge var præcis det, jobbet blev
bedt om.
Lektien er ikke den linje. Den er, at "udgiv senere" har brug for noget, der
fejler, når senere kommer. En kontakt, der er slukket og tavs, ligner en kontakt,
der er tændt.
Udgives nu fra main og ved håndsoprækning, aldrig fra en pull request — en fork
kunne ellers udgive hen over siden ved at åbne en.
Dokumentationen manglede ⌘K-ændringen fra i går: at vælge en note fra paletten nu
efterlader de andre træffere i listen ved siden af den. Den står i "Finding",
hvor de to søgefelter i forvejen er beskrevet hver for sig uden at sige, at de nu
er enige.
README'en manglede tolv ting, der er bygget siden tabellen sidst blev rørt — og
forseglingen af noter, som kun stod i docs. At en kopi af databasefilen ikke er
en kopi af noterne, er en overskrift for et selvhostet program, ikke en detalje
på side tre.
GitHub-repoet havde ingen hjemmeside og ingen emner, og beskrivelsen nævnte
hverken noter, kalender eller AI. Alle tre er sat.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
I arket er et billede skaleret ned til spaltens bredde — et foto fra en telefon
er fire tusind pixels bredt — så det, man kigger på, er et miniaturebillede, der
ikke ser ud som et. Der var ingen vej fra det til billedet selv.
Et klik åbner det nu i fuld størrelse, med en knap, der kopierer det, og et link,
der åbner det i sit eget faneblad. Escape lukker og sætter markøren tilbage i
teksten — ellers står fokus på en knap, der ikke findes længere, og den næste
tast går ingen steder.
Klikket ligger i onEditorClick ved siden af notelinket, som er det andet, man kan
klikke på inde i et felt, man ellers skriver i. Billedet bliver stadig markeret
af klikket, og det skal det: det er sådan, det slettes.
To ting, der ikke er til at gætte:
Udklipsholderen tager kun PNG i de fleste browsere, og et foto fra en telefon er
en JPEG. Så et billede, der ikke allerede er PNG, tegnes om over et lærred —
ellers lykkes skrivningen enten ikke, eller den lykkes med noget, ingen kan
indsætte. Prøven indsætter derfor med vilje en JPEG; med en PNG var den kørt
forbi netop den kode.
Og løftet gives videre til ClipboardItem frem for at blive ventet af først.
Safari kræver, at clipboard.write kaldes i selve klikket, og en hentning
imellem gør klikket til fortid.
Ruden ligger uden for .wrap: editoren har sin egen rulning og sin egen
stablingskontekst, og et lag inde i den ville blive klippet af netop det ark, det
skal lægge sig over.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paletten viste de noter, der passede, i sin egen liste — og når man valgte en,
åbnede den noten på en side, hvis liste stod helt uændret. De andre træffere var
væk, og det eneste sted, de nogensinde havde været, var en popup, der lige var
lukket.
Feltet i selve notelisten har filtreret hele tiden, og det er prøvet to steder.
Men den søgning, man finder, er den store i toppen, og den er palettens. Så det
er ikke "søgning i noter virker ikke" — det er, at den søgning, man bruger, ikke
efterlod noget.
Adressen bærer nu søgningen. ⌘K sender `?note=…&q=…`, notesiden læser begge, og
listen ved siden af den åbne note er de andre træffere. Den skrives tilbage, når
feltet ændres, så adressen kan gemmes og deles — og frem for alt ikke lyver, når
feltet er tømt.
To effekter, der deler en markør, og hvorfor de læser den med `untrack`:
Den nederste sætter `settled` og kalder `replaceState`. Det vækker den øverste,
og på det tidspunkt har `$page.url` endnu ikke skiftet — så den læste den gamle
adresse, fandt en søgning, der ikke var enig med markøren, og skrev den tilbage
i feltet. Man tømte søgefeltet, og ordet kom igen. Hver effekt skal reagere på
sin egen kilde, adressen og feltet, og aldrig på den fælles markør.
Den fejl fandt jeg kun, fordi prøven fejlede på den sidste linje og
fejlbilledets tilgængelighedstræ stadig viste "Fyrreskov" i feltet. En prøve,
der kun havde tjekket adressen, havde været grøn.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Den skulle have gjort det siden #72, og gjorde det ikke. v0.45.0 og v0.46.0 fik
begge flettecommittens overskrift som udgivelsestekst — uden at noget fejlede,
og uden at nogen kunne se det andet end på siden bagefter.
`fetch-depth: 0` er ikke nok. actions/checkout henter først alle tags korrekt og
overskriver derefter netop den ene, der udløste kørslen:
git rev-parse refs/tags/v0.46.0
git fetch --no-tags origin +0dc4c2f9b8…:refs/tags/v0.46.0
Den henter commit-SHA'en ind i tag-refen. Den annoterede tag bliver dermed til
en letvægtstag, der peger direkte på commit'en, og `%(contents)` giver så
commitbeskeden. Kommentaren i filen sagde, at en overfladisk checkout var
problemet; det var rigtigt og utilstrækkeligt.
Tagget hentes derfor som *ref* igen, lige før det læses.
Og vagten: hver tagbesked begynder med sin egen version, så holder det ikke,
læser vi noget andet end tagget, og kørslen fejler frem for at udgive en forkert
tekst. Det er det, der manglede — fejlen var ikke svær at rette, den var svær at
opdage.
Målt i et klonet arbejdstræ i alle tre tilstande: annoteret giver den rigtige
tekst med selvlinket fjernet, letvægt giver flettecommitten og udløser vagten,
og en re-fetch af refen bringer den annoterede tilbage.
Teksten på de to udgivelser, der allerede stod forkert, er rettet i hånden.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Spørgsmålet var, om modtageren ikke burde godkende først. Svaret er nej — men
det, spørgsmålet fik øje på, var rigtigt, og værre end det lød.
Tre ting gjaldt samtidig: man blev tilføjet uden at blive spurgt, man fik ikke
besked, og man kunne ikke gå ud igen. En delt note gav en klokkebesked; et delt
projekt gav ingenting — det stod bare i sidebjælken næste gang, man kiggede,
uden at noget sagde hvornår eller fra hvem. Og handleRemoveMember lå bag
ejer-kravet, så kun ejeren kunne fjerne nogen, også en selv.
Hvert enkelt af de tre er til at leve med. Alle tre sammen gør en deling til
noget, der sker for en.
Ikke et godkendelsestrin: en invitation, der venter på et ja, betyder, at den,
der delte, tror det er gjort, mens arbejdet ligger stille — og noter deles
allerede uden. Det, der manglede, var besked og en udgang.
`project.shared` er ny. Klokkens follow() kunne allerede føre et project_id hen
til projektet, så beskeden bærer sit eget mål og krævede ingen særregel.
Ruten til at fjerne et medlem er flyttet ud af ejer-gruppen, og handleren skiller
nu de to ting ad: ejeren, der fjerner nogen, og den, der selv går. At pege på en
andens medlemskab uden at eje projektet svarer 404 og ikke 403 — "det må du
ikke" bekræfter, at medlemskabet findes. Ejeren kan stadig ikke gå fra sit eget
projekt; det ville være en overdragelse, og den findes ikke endnu.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
Ø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>
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>