Meldt ind som "den er for tæt på en eller anden måde". Grunden var ikke afstanden
mellem rækkerne: hver række sagde tre ting, og to af dem gentog sig selv nedad.
Datoen stod på hver eneste række — også når tyve noter deler dag — og projektet
fik en linje for sig selv. Tre linjer pr. note, gange tolv hundrede.
Gentagelserne er flyttet derhen, hvor de siger noget.
**Datoen er blevet til overskriften.** I dag · I går · Denne uge · August 2026 …
og så er den væk fra rækken. Noten går fra tre linjer til to, og luften kommer af,
at der er mindre at skrive — ikke af at listen bliver længere.
**Favoritterne har fået deres egen gruppe.** De lå allerede først; der stod bare
ikke hvorfor.
**Grupperingen følger sorteringen.** Sorteret på "Oprettet" grupperes der på
oprettelsesdatoen, på "Senest rørt" på den — ellers ville listen stå i én
rækkefølge og være grupperet efter en anden, og så er overskrifterne løgn.
Sorteret på navn er der ingen dato at gruppere på, så det bliver forbogstavet, og
datoen kommer tilbage i rækken, hvor den så er det eneste tilbage, der siger noget.
**Arkivet grupperes med.** Det er dér, de tolv hundrede importerede ligger, og
dér, det betyder mest.
**Projektet har sin egen farve** — den samme prik som i sidebjælken. Mærkaten sagde
det samme som alle de andre i en grå, der var lysere end uddraget, og projektet er
dét, man skanner efter, når man leder i tolv hundrede noter. Prikken bærer farven
og ikke teksten: fem projektfarver som skriftfarve er fem forskellige grader af
læsbarhed, og den lyseste er ikke læsbar.
Fire ting, der blev fundet ved at se på det:
- Uddraget gentog titlen, fordi titlen *er* første linje. Det var skjult, så længe
en dato stod imellem dem.
- Uddraget klippes ikke af sig selv. Det lå før i en flex-boks, der ejede
afkortningen; da datoen flyttede op, forsvandt boksen, og sætningen løb videre
ind under favoritstjernen.
- Overskriftens stjerne hed `.star` ligesom rækkens favoritknap og arvede dens
gennemsigtighed. Den stod der aldrig. To ting, der ligner hinanden, er ikke det
samme.
- En `aria-label` på foldeknappen erstatter knappens indhold som dens navn — og
knappen *er* overskriften, så overskriften kom til at hedde "Fold I dag sammen".
Sidebjælkens gruppehoveder gør det rigtigt, og gør det nu begge steder.
Sammenfoldning huskes ikke på tværs af besøg: en gruppe, der åbner foldet uden at
nogen bad om det, er en liste, der ser tom ud.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Alt om det var usynligt. Indstillingssiden sagde "til for denne enhed", så snart
der fandtes et abonnement — også når hver eneste push blev afvist. Og en afvist
push gik i en logline på **Debug**, som ikke skrives på en instans, der kører på
standardniveau. Så en levering, der fejler lydløst, og en levering, der aldrig
sker, så ens ud, og forskellen er hele diagnosen.
Tre forskellige problemer havde ét udseende: browseren tilmeldte sig aldrig,
tjenesten afviste den, eller der var ikke noget forfaldent endnu.
**En prøveknap** under Indstillinger → Notifikationer sender én notifikation
gennem den rigtige rute — ikke en genvej, så det, den beviser, er det, der
faktisk skal virke — og siger hvad hver enhed gjorde ved den. Kom den ikke frem,
og afviste ingen den, står der, at det så er browseren eller styresystemet, der
holder den tilbage. Nu peger svaret på ét af de tre steder.
Loglinjen er hævet fra Debug til Warn. Det var den ene linje, der ville have sagt
hvorfor.
Teksten kommer fra klienten, fordi ordbøgerne gør. En server, der bærer sin egen
kopi af "det her er en prøve", bærer den på ét sprog.
**Og en lækage, prøven fandt.** Go pakker en transportfejl som `*url.Error`, og
dens tekst er `Post "https://fcm.googleapis.com/fcm/send/dQw4…": dial tcp: …` —
hele endepunktet, som er den legitimation, push-tjenesten udstedte til den enhed.
Den var på vej ud i svaret, i loglinjen og i ethvert skærmbillede af nogen af dem.
Prøven var skrevet for at bevise, at kun værten blev nævnt, og det var sådan den
blev fundet. Fejlen pakkes ud, og adressen skrubbes bagefter alligevel: den næste
fejltype, der bærer den med, kommer ikke med en advarsel.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bedt om: "jeg vil have tid på opgaver, og jeg vil kunne justere dem i kalenderen."
Serveren kunne det hele i forvejen — `due_date` og `due_time` læses sammen og
bliver til et `due_datetime` i kontoens tidszone. Det var fladen, der ikke havde
nogen vej til at sige det.
**Et felt i opgaveruden.** Tomt, indtil nogen sætter det: en opgave uden
klokkeslæt er det almindelige, og et felt, der åbner på 00:00, påstår, at noget
skal ske ved midnat.
**Og en fejl, der lå og ventede.** Serveren læser en dato uden en tid som "ryd
tiden". Så et gem af datoen alene tog klokkeslættet med sig — man rettede dagen og
mistede timen — og det samme gjorde hvert eneste træk i kalenderen. Ingen af
delene sagde noget.
`reschedule` siger nu tiden højt, tre måder, fordi de to første ligner hinanden i
JavaScript og derfor skal skilles ad ét sted frem for at blive forvekslet fire:
undefined behold den, opgaven har — månedens ruder, hvor der ingen tid er at læse
'' ryd den — heldagsbåndet
'HH:MM' sæt den — et sted på døgnet
**Hvor på søjlen man slipper, er klokkeslættet.** Målt mod søjlens egen kasse frem
for mod siden, så det bliver rigtigt uanset hvor gitteret sidder og hvor langt der
er rullet. Rundet til kvarter: minuttet under markøren er ikke en oplysning, nogen
har — pegefingeren er bredere end et minut — og 14:07 er et tal, der ser ud som om
det blev valgt.
Prøven går hele vejen: sæt tiden, genindlæs og se at serveren tog den, ret datoen
og se at timen bliver, find opgaven på sit klokkeslæt i ugegitteret, og træk den op
i båndet igen. To ting, den lærte undervejs, står skrevet ved den: et træk over
fire hundrede pixels skal gå i skridt, ellers har browseren for få mellemstationer
til at holde gesten i live — og klokken skal være tidlig, for klokken fjorten
ligger under skærmkanten, og at måle noget dernede ruller siden væk under målet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
To ting, meldt ind fra en uge, der ikke svarede på det, man åbnede den for.
**Dagene var ikke lige brede.** Gitteret stod på `repeat(7, 1fr)`, og `1fr` er
`minmax(auto, 1fr)`: `auto` som minimum betyder, at en søjle ikke kan blive
smallere end sit længste ord. Én opgave, der hed "Gårdbutikken på Møllerup Gods —
opfølgning på mail", gjorde onsdag dobbelt så bred som mandag og klemte resten af
ugen sammen om den. `minmax(0, 1fr)` lader søjlen blive så smal, den skal, og så
klipper brikken sin tekst af — hvilket den allerede kunne.
**Ugen var syv punktopstillinger.** Den svarede på "hvad skal der ske torsdag" og
ikke på "hvornår", og det andet er det, man åbner en uge for. To møder klokken ti
er en konflikt, man skal kunne se, ikke læse sig til.
Nu et gitter med en tidsrende og syv dage: timerne ned ad siden, begivenheder
placeret efter klokkeslæt, og et bånd foroven til det, der ikke har et — en
heldagsbegivenhed lagt klokken nul ville påstå, den holdt fra midnat til midnat.
Overlappende ting deler bredden i sammenhængende klynger frem for parvist:
overlapper A med B og B med C, skal alle tre dele, også når A og C ikke rører
hinanden. Ellers ville A og C hver tro, de havde halvdelen, og lægge sig oven på
hver sin halvdel af B.
Hvilke timer der tegnes, kommer fra ugens egne begivenheder med arbejdsdagen som
gulv. Et helt døgn er fireogtyve rækker, hvoraf de otte altid er tomme, og så er
gitteret noget man ruller i frem for noget man ser — men en vagt klokken fem skal
med, uden at alle andre uger betaler for den med tomme rækker.
Måneden er ikke rørt ud over søjlebredden. En dag i en månedsrude er halvanden
centimeter høj; et døgn tegnet i den er ikke en visning, det er en streg.
Brikkerne er flyttet ud i to snippets, som begge visninger tegner med. De viser
de samme to ting og skal vise dem ens; det er placeringen, der er forskellig, og
så er det kun placeringen, der står to steder.
`data-date` følger med over på ugens timesøjler — det er sådan noget uden for
komponenten peger på en dag. Båndet får sit eget `data-allday`, så de to ikke
bliver til det samme svar på ét spørgsmål.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der er hverken en e-mail eller en adgangskode i et passkey-login. Loginet er
discoverable — enheden ved selv, hvilken konto nøglen hører til, og der bliver
ikke tastet noget. Alligevel fik en afvisning fladens generiske 401-tekst, og den
nævner begge dele.
Det sendte en person ud at lede efter en tastefejl i sin konto, der aldrig havde
været der. Det, der faktisk var sket, var at instansen var flyttet fra http til
https: en passkey hører til præcis den adresse, den blev lavet på — `RPOrigins`
bygges af `VERDANDE_BASE_URL` med skema og alt — så et login på det ene skema mod
en server, der venter det andet, kan ikke verificeres.
Beskeden siger nu det, og siger også hvor man skal kigge. Serveren har hele tiden
svaret "that key was not accepted"; det var oversættelsen i fladen, der gjorde
den til noget andet.
Aflysning bliver behandlet som før — den er ikke en fejl, og en fejl for den ville
læse som om nøglen blev afvist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Meldt ind som "jeg kan ikke skifte mellem noter — uanset hvilken jeg trykker på,
åbner de ikke".
Editoren indlæste med `note.body.trim()` uden værn. `body` er en streng fra
serveren hver eneste gang — indtil den ikke er, og så kaster den her inde i et
`$effect`. En effekt, der kaster, tager komponentens opdateringer med sig: den
forrige note bliver stående, og *ingen* note kan åbnes bagefter. Klikket kan ikke
engang lande, hvilket er derfor det ikke ligner en fejl i noten, man klikkede på,
men i hele ruden.
To ting, ikke én. Værnet, så en manglende krop er en tom note. Og en ramme om
tegningen, så det næste, der en dag ikke kan tegnes, heller ikke bliver til det
her: en note, der fejler, viser sin egen tekst uden formatering, og resten af
ruden virker videre.
`loadedId` sættes nu før tegningen frem for efter. Ellers ville effekten prøve
igen ved hver eneste ændring bagefter og kaste hver gang.
`textContent` og ikke `innerHTML` i faldbakken: teksten er ikke tegnet, og at
lægge den ind som markup ville være at køre præcis det, tegningen nægtede.
Gemningen er ikke i fare — den skriver `draft`, som kommer fra svaret og ikke fra
det, der står i editoren, indtil nogen taster i den.
Prøven tvinger formen frem i stedet for at vente på den nyttelast, der gjorde
det. Hvad der end fik `body` til at mangle, er svaret det samme: én note må ikke
kunne låse de tolv hundrede andre ude. Den er set fejle uden rettelsen — med
netop den fejl, konsollen viste.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Det er `.page`, der ruller — og det var også `.page`, der blev smallere for at
holde linjelængden nede. Så bjælken fulgte med ind til tekstsøjlens kant og stod
med en håndflade tom rude til højre for sig, en tredjedel inde i ruden.
Grænsen sættes som polstring i stedet for som bredde. Elementet er lige så bredt
som ruden — bjælken bliver ved kanten, hvor den hører til — og teksten er
nøjagtig lige så bred som før. `max()` mod nul, fordi en smal rude ellers ville få
negativ polstring, og `border-box` er sat globalt, så bredden er hele boksen og
ikke boksen plus polstringen; uden det ville det give en vandret rullebjælke.
Kildevisningen deler `.page` med editoren og havde samme fejl. Den følger med.
Målt: elementet slutter nu på rudens indholdskant i stedet for 74 px inde, og
`scrollWidth - clientWidth` er nul.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rækkefølgen af sektioner *er* planen — "Planlægning" står før "I gang", fordi det
er den vej arbejdet går — og indtil nu kunne den kun laves om ved at slette en
sektion og lave den igen, hvilket smider dens opgaver ud i det usektionerede
område på vejen.
Hele overskriften er håndtaget. En sektionsoverskrift indeholder ikke noget, der
selv bliver trukket, så der er ingen geste at stjæle — hvilket er grunden til, at
sidebjælkens rækker skulle have et greb, og grunden til at disse ikke skal. Ikke
draggable mens den omdøbes: et draggable element lader ikke markøren vælge tekst
i det felt, det indeholder.
Serversiden er hele listen på én gang som projekter og grupper, og af samme
grund: et projekt har en håndfuld sektioner, så 0, 1, 2 … er én lille transaktion
uden præcision at løbe tør for, og en liste kan ikke ende halvt anvendt.
`ReorderSections` er scopet til projektet og ikke til en ejer — en sektion har
ingen ejer, den hører til et projekt, og om du må rykke rundt er et spørgsmål om
det projekt, som handleren allerede har stillet. Id'er fra et andet projekt
springes over af WHERE-klausulen; der er en prøve på præcis det, fordi ingenting
ellers kontrollerer id'erne enkeltvis.
Båndet stod fire pixels skævt. Det var trukket ud med --s2 og polstret med --s3 —
samme idé gjort med to forskellige tal — så det blev otte pixels bredere end
listen i hver side, og teksten landede fire pixels uden for den kolonne, rækkerne
holder. Til venstre var det usynligt. Til højre var det Omdøb og Slet, der stod
fire pixels forbi alt over og under sig, hvilket er præcis langt nok til at ligne
en fejl og ikke langt nok til at se hvorfor. En række er `.row` med
`padding: var(--s2)` inde i en boks uden, så båndet har nu samme polstring og
ingen margen.
Board-visningen er ikke rørt: der er kolonnerne vandrette, og det er en anden
geste.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hele sidebjælken rullede, så runen og I dag forsvandt over kanten, så snart der
var projekter nok — og de er dem, man er på vej hen til oftest. Alt under de
faste visninger ligger i en rulleboks nu, så rullebjælken begynder under dem.
En boks indeni frem for `position: sticky` udenom: et klæbende hoved efterlader
rullebjælken i fuld højde ved siden af sig, og det var netop den, der skulle
starte længere nede. Snippet-erklæringerne bliver liggende udenfor — de tegner
ikke noget selv, men de kaldes oppe fra .views, og en snippet erklæret indeni
hører kun til indeni.
Chevronen for en mappe dækkede gruppens prik med et felt i --surface. På den
sænkede bjælke er det en lys firkant, og en foldet gruppe har sin chevron fremme
permanent, så klodsen stod der hele tiden. Prikken tones ud i stedet.
Det kostede en fælde: en `opacity` under 1 laver en stakkontekst og maler
elementet, som var det placeret med `z-index: 0`. Prikken lagde sig dermed oven
på knappen, den lige var forsvundet for, og tog klikket — usynlig og stadig i
vejen, så gruppen ikke kunne foldes. Ét lag op på knappen, og rækkefølgen er
som før.
Prøven for vandret rul måler nu begge bokse. Bjælken selv er `overflow: hidden`,
så en for bred række ville blive klippet dér og aldrig nå at tælle med — den
ville bestå af den forkerte grund.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Den skiller "skriv en ny" fra "det, der allerede er". Uden afstand sad den første
sektion klods op ad den, så den læste som en kant om sektionen frem for som en
afslutning på feltet over. En streg skal have plads på begge sider for at kunne
ses som en streg.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
To knapper ved siden af hinanden er toogfyrre pixels taget fra en liste, der er
tre hundrede bred — og de er det, man rører en gang imellem, mens titlen og
uddraget er det, man læser hele tiden. I en søjle koster de toogtyve.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
46rem var sat for at holde linjelængden i skak, og det er stadig det rigtige
hensyn — men det blev sat som ét tal, og på en skærm halvanden gang så bred stod
teksten i en smal søjle med en håndflade tom plads ved siden af. Kodeblokke og
billeder, som er de to ting i en note, der *vil* have bredde, blev klemt sammen
uden grund.
clamp frem for et tal: en smal rude får det hele, en bred får en linjelængde,
øjet kan finde tilbage i. På 1920 er tekstsøjlen nu 1088 px af arkets 1304 i
stedet for 736.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
**Arkivet, fordi papirkurven er det forkerte redskab.** Den siger "det her var en
fejl, og om tredive dage er det væk" — så alt, der er værd at beholde, kan ikke
komme i den. Et arkiv på tolv hundrede importerede noter er præcis det tilfælde,
hvor de fleste er værd at beholde og næsten ingen er værd at kigge på; uden et
sted at lægge dem er listen hele arkivet for evigt, og at finde ugens note
betyder at læse forbi et årti.
En kolonne, ikke en mappe. Noterne afviste et andet hierarki i 0018, og
arkivering er ikke arkivering i den forstand: det er én oplysning om, hvorvidt en
note er aktuel, og den skal kunne besvares ved siden af `deleted_at`, som er den
anden af samme slags. `updated_at` røres ikke — at rydde op er ikke at skrive.
**Flere ad gangen.** ⌘-klik lægger til, skift-klik tager intervallet i den
rækkefølge, listen faktisk står i — ikke notens egen, som er en anden, når der er
sorteret eller søgt. Trækker man en note, der er markeret, følger hele
markeringen med; trækker man en, der ikke er, er det den ene, for ellers ville et
træk i en tilfældig række flytte halvtreds noter, man havde glemt var markeret.
Ét kald frem for et pr. note: efter en import af tolv hundrede er det at vælge
halvtreds det almindelige tilfælde. Hver note kontrolleres stadig for sig — et sæt
er en bekvemmelighed for den, der kalder, og aldrig en vej til noget, de ikke
kunne have rørt én ad gangen.
En kant *og* en tone på det markerede: `--accent-sunken` er meget lys i lyst tema,
og tre markerede rækker var svære at skelne fra tre, der ikke var — en dårlig ting
at være i tvivl om lige inden man trykker Slet.
**Kopiér-knappen spiste næsten den kode, den skulle kopiere.** Den ligger inde i
`<pre>` for at kunne stå i hjørnet af blokken, og `colourCode` læste
`pre.textContent` og malede resultatet tilbage — så et klik på "Kopiér" skrev
"Kopieret" ind i koden, og næste gemning lagde det i filen. Nu ét sted, der siger
hvad en kodeblok indeholder, brugt af både farvelægningen og oversættelsen til
Markdown: var de to uenige, ville forskellen ende i noten. Prøven klikker,
læser udklipsholderen og læser filen bagefter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Chrome frøs på notesiden, og der var to grunde.
**Nyttelasten.** Listen viser en titel og omkring halvfems tegn. Den fik hele
noter for at gøre det: med tolv hundrede blev svaret 3,9 MB, hvoraf otteoghalvfems
procent var brødtekst, ingen ville læse på den skærm — hentet, tolket og holdt i
hukommelsen hver gang siden blev åbnet. Nu 763 KB.
Kroppen tømmes frem for at blive forkortet, og det er den vigtige halvdel. Et felt,
der hedder `body` og indeholder et *stykke* af kroppen, er en fælde: editoren ville
åbne det, pausen ville gemme det, og noten ville være skåret ned til sit eget
uddrag af ingenting andet end at være blevet kigget på. Tom er en form, klienten
kan se efter — og den gør det og henter hele noten, når den åbner en.
Det kostede en adskillelse mere: hvilken række der er *valgt*, og hvilken note der
er *indlæst*, er nu to ting. Editoren indlæser sin tekst, når notens id skifter, så
hvis den fik uddraget først og hele noten bagefter, ville id'et være det samme
begge gange og den anden aldrig blive vist. Man ville sidde med en tom note, der
lige havde haft en titel.
**Tegningen.** Alle tolv hundrede rækker fik layout og maling, også de tusind
ingen kan se: 2,1 sekunder til første tegning. `content-visibility: auto` lader
browseren springe det over og tage det, når man ruller derhen — 0,95 sekunder, og
uden en eneste linje JavaScript, ingen rullematematik og ingen liste, der kan komme
ud af trit med sig selv.
**Og søgefeltet.** Det var en pille med et forstørrelsesglas tegnet ind som et
baggrundsbillede med farven skrevet i hånden — #8b918d, uanset tema — hvilket
gjorde det til det ene sted på siden, der ikke fulgte resten. Nu samme form som
søgefeltet i toppen, et rigtigt ikon i `currentColor`, en ryd-knap og et antal
fundne.
Det havde heller ingen pause: hver tast sendte en søgning gennem tolv hundrede
noter, så "kaffe" var fem forespørgsler, hvoraf de fire var forældede inden de kom
tilbage — og svarene kunne lande i den forkerte rækkefølge. Nu én.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
**Søgningen sorterede efter dato.** Usynligt med ti noter og ubrugeligt med tolv
hundrede: de blev alle importeret samme aften, så `updated_at` siger ingenting, og
loftet skar listen af ved halvtreds vilkårlige rækker. At søge "Tesla" svarede med
en note om nginx først og noten, der hedder "Tesla Model Y", bagefter — hvilket
læses som en søgning, der ikke virker, fordi det fra brugerens plads er præcis
dét.
Nu bm25, med titlen vægtet ti gange kroppen: et ord i titlen er, hvad noten
*handler om*, mens det samme ord én gang i en lang krop er en henvisning. `fold`
vejer som kroppen, ellers ville et dansk match slå et præcist. Sammenføjet frem
for `rowid IN (…)`, fordi rangeringen hører til matchet, og en underforespørgsel
smider den væk. Fladen sorterer ikke om, når der er søgt — det ville kaste netop
det væk, der gør en søgning i tolv hundrede noter brugbar.
**Et træk af en note hen på et projekt gjorde ingenting.** `ondragover` tog imod
både opgaver og noter, så rækken lyste op — og `ondrop` sendte kun opgaver videre,
mens noten faldt ned i grenen, der omarrangerer projekter, hvor den ikke betyder
noget. Den værst mulige form for i stykker: den så ud, som om den ville virke,
helt indtil man slap.
Det står i overleveringen som "virker i koden, men ikke fastholdt i prøver, fordi
prøven blev upålidelig". Den er skrevet igen, og den ser på resultatet — ligger
noten i projektet bagefter — frem for på om rækken blev fremhævet, hvilket var
netop det, den gamle kunne se og som blev ved med at være sandt, mens funktionen
var i stykker.
**Og datoen i listen var altid `updated_at`.** For tolv hundrede importerede noter
er det i aftes, så en note fra 2017 stod som "i går", og den dato, man sorterede
efter, var ingen steder at se. Rækken viser nu den dato, listen er sorteret efter,
og har begge i sit værktøjstip.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
To huller fra sikkerhedsgennemgangen, og de er samme historie fortalt to gange:
den ene kontrol, der findes til når adgangskoden allerede er tabt, kunne omgås af
netop den, der havde adgangskoden.
**H1 — spanden blev fyldt af adgangskoden alene.** `handleLogin` nulstillede
ratebegrænseren ved hver korrekt adgangskode, *før* den så på, om der var en
andenfaktor. Det gav én, der havde kodeordet, ubegrænsede portioner på ti gæt:
log ind, få en frisk spand og en frisk ventende session, brug ti gæt på koden, log
ind igen. Med tre gyldige koder i ethvert øjeblik og tyve forespørgsler i
sekundet er det lige odds inden for en eftermiddag.
Spanden ryddes nu først, når login er *færdigt*, og der er kommet en spand mere:
koder tælles pr. konto og ikke kun pr. adresse. Adressespanden er slået af enhver
med mere end én adresse, hvilket er det almindelige — en telefon og en bærbar er
allerede to. Det, der gør en sekscifret kode sikker, er at gæt er en knap
ressource, og knapheden skal tælles imod det, der bliver angrebet.
Den ventende session får lov at leve. At afslutte den ved første forkerte ciffer
blev overvejet og valgt fra: det ville koste en, der taster ét tal forkert, sin
adgangskode igen, og det køber næsten ingenting oven i loftet pr. konto.
**M3 — et lækket token kunne tilmelde sin egen andenfaktor.** `/auth/totp/setup`
lå kun bag `requireAuth` og spurgte om ingenting, mens `disable` altid har
krævet adgangskoden: døren åbnede med et token og lukkede kun med et kodeord. Et
stjålet token kunne hente hemmeligheden, lægge den i sin egen godkender, bekræfte
den og beholde den eneste kopi af gendannelseskoderne. Ejerens næste login ville
stoppe ved en kode, de ikke havde, og de kunne ikke slå den fra uden at logge ind
først. Et tyveri bliver til et lejemål — den sætning står allerede i filen ved
siden af om passkeys, og det er den samme regel.
Nu bag `requireSession` sammen med passkeys og sessionerne, og `setup` spørger om
adgangskoden ligesom `disable`. Symmetrien er pointen: at slå den til er mindst
lige så indgribende som at slå den fra.
Og en note kan oprettes inde fra et projekt. At ligge i et projekt *er* måden en
note deles på, så det er én handling frem for to — man skulle før oprette noten
under Noter og bagefter finde projektet frem i "Del i". Ruden findes nu, også før
der er en eneste note, for ellers er der ingen knap dér, hvor man opdager at der
skal skrives noget ned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Læs bredt, skriv snævert — decision 5 i planen, og kun den første halvdel:
kalenderen læses, og der skrives ingenting tilbage.
**OAuth-blokeringen er reel og blev ikke gået udenom.** En klient registreret som
*Internal* tager kun konti i sit eget workspace — også til kalendere, ikke kun
Gmail. Andreas' private konto bliver afvist med `org_internal`, og det er
klienten, der virker efter hensigten, ikke en fejl. Fladen siger det nu med ord,
man kan handle på, frem for at vise Googles egen skærm, der ikke gør.
Vejen udenom, som ikke er bygget: kalenderens egen hemmelige ICS-adresse. Ingen
OAuth, virker for enhver konto, og `internal/ics` findes allerede. Det er det
næste stykke, og det eneste, der låser den private konto op.
`internal/google` er flyttet ud af `internal/gmail`, fordi de to er det samme
flow, der er uenige om én streng. Egen callback-adresse frem for at dele Gmails:
én callback, der finder ud af hvilken token-butik den skal skrive i, går galt ved
at overskrive en Gmail-forbindelse, der virkede.
Egne tabeller frem for `mailboxes`. `trigger_kind`, `seen` og `last_uid` svarer
alle på "hvad gør en besked til en opgave", og ingenting her bliver til en
opgave; én konto har mange kalendere, og `mailboxes` har ikke plads til en
en-til-mange. Afgørende i praksis: `mailboxes.kind` har en CHECK, og SQLite kan
ikke ændre en — en tredje værdi koster den tabelombygning, som migration 0021
skulle igennem.
Begivenheder erstattes pr. kalender pr. vindue frem for at flettes med et
sync-token: et vindue, der slettes og skrives forfra, kan ikke komme ud af trit,
og et sync-token er endnu en ting, der skal være rigtig for altid, for at gitteret
ikke stille viser noget forkert.
`CalendarView` er udvidet, ikke kopieret. En begivenhed er et fyldt bånd, hvor en
opgave er et kort med en farvet kant, så de to læses forskelligt før et ord er
læst — og den kan ikke trækkes og er aldrig en knap: der er ikke noget at gøre
færdigt ved en andens møde.
To værn på det, der kommer fra Google: farven skal matche et hex-mønster, før den
må stå i en style, og linket bliver kun til et href, hvis det begynder med https.
Konflikten i jobs.go var to nye jobs skrevet på samme linje. Begge skal med.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Skærmbillederne er engelske, fjorten i alt, og der er to mennesker i dem. Det
sidste er ikke pynt: halvdelen af det, programmet er til, findes ikke med én
konto — en ansvarlig, et navn i historikken, en medlemsliste, og "Venter på
andre" som andet end en tom side.
Sproget kommer fra kontoens locale og ikke fra strenge skrevet i hånden, hvilket
er dét, der fandt fejlene: otte steder, hvor grænsefladen blev ved med at være
dansk, uanset hvad kontoen sagde. "Forsinket" og "I dag" skrevet ind i markup,
"Uden sektion" som boardets første kolonne, gentagelsesreglerne halvt oversat,
alle nitten navne på hændelser i historikken, og et `da-DK` hårdkodet i Kommende.
Gentagelsesreglerne er flyttet til `when.js`, så menuen og mærket ikke kan blive
uenige igen — det er præcis, hvad den fil er til.
README's billeder havde været brudte siden `37605ba`, som lavede PNG'erne om til
WebP og slettede dem, uden at teksten fulgte med: forsidebilledet og alle seks i
tabellen var 404 på GitHub. Og noterne — hele funktionen — stod ikke nævnt hverken
i README, på forsiden eller i dokumentationen.
Nu skrives PNG til docs og WebP til site, altså præcis dét, de to steder læser.
Før efterlod hver kørsel tolv usporede PNG'er i site, som intet pegede på.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hjælpeteksten sagde "slå Gmail API til", og det er kun det halve nu. Den samme
klient bærer både Gmail og Google Kalender og skal have begge redirect-URI'er —
og kalenderpanelet siger, hvor dets egen kommer fra, så ingen leder efter et
felt til en klienthemmelighed, der allerede står ovenfor.
Rapporteret fra brug: man kunne ikke indsætte billeder eller trække filer ind i
noter. Det passede — importen kunne tage billeder med, og intet andet kunne.
Indsætningen tog kun `text/plain` og smed resten væk, og der var slet ingen
håndtering af et træk. At indsætte et skærmbillede er det mest almindelige, der
findes i et noteprogram, og det gjorde ingenting overhovedet.
Der manglede også et sted at lægge filen. Vedhæftninger på en note fandtes i
databasen, siden migration 0021, men kun importen kunne skrive dem; der var ingen
rute. Nu er der en, med editor-rettigheder på noten og samme svar for "findes
ikke" og "ikke din", så et id ikke kan afprøves.
Skrevet ind som `` og ikke som en data-URL: en note er Markdown i en
database, og et fotografi fra en telefon som base64 midt i teksten ville gøre
noten til fire megabyte tekst, der skal læses hver gang listen tegnes.
`execCommand` frem for at bygge noden selv, fordi det er dét, der lægger
handlingen i browserens egen fortryd-historik — ⌘Z tager billedet ud igen,
ligesom det tager et ord ud.
`preventDefault` på både dragover og drop. Uden den første kommer der intet drop;
uden den anden åbner browseren filen i stedet, og man mister det, man var i gang
med at skrive, til en fane med et billede i. Kun filer: et træk inde fra appen
selv bærer vores egen nyttelast og skal falde igennem til den, der lyttede.
Prøven går begge veje ind, ser at billederne bliver hentet og tegnet, og læser
filen bagefter for at sikre, at der står to bilagsadresser og ingen data-URL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Serveren sorterer allerede, men en celle, der viser 14:00 over 09:15, ligner
ikke en liste i forkert rækkefølge — den ligner et forkert klokkeslæt på
chippen, og det er langt værre at skulle udelukke. Heldags først: en heldags
begivenhed er et bånd hen over dagen, ikke noget der sker på et punkt i den.
At vælge en kalender er nogen, der siger "vis mig den her". At gemme valget og
lade gitteret stå tomt, til fejeren kommer forbi et kvarter senere, er en
kalender, der påstår, at fjorten dage er fri — og personen står på siden med
knappen under hånden. Gemningen henter nu selv.
Og er der forbundet en konto uden at der er valgt nogen kalendere, siger
visningen dét. "Der er ikke valgt en kalender" og "der er ikke booket noget"
ser ens ud, og kun den ene er noget at gøre noget ved.
Landingssiden og dokumentationen læses på engelsk, og billederne var danske
hele vejen — både grænsefladen og indholdet. Kontoen bliver nu oprettet med
engelsk sprog frem for at oversætte noget her: det er sådan appen selv gør
det, og det er også dét, der får en glemt oversættelse til at stå i et
billede i stedet for at vente på en bruger.
Demodataene er skrevet om til engelsk arbejde og husholdning. Og der er en
person mere: en invitation, en konto, tre opgaver hun sidder på og en
kommentar. Halvdelen af det, appen er til for, findes først, når der er to i
den — en ansvarlig på en opgave, et navn i historikken, og "Venter på andre"
som andet end en tom side.
Tre billeder viste ikke det, teksten sagde. `detail` var en tom formular, så
opgaven har fået beskrivelse, undertasks, en fil, en påmindelse og en samtale.
`themes` viste profilfeltet over temaerne, så den ruller nu ned til dem.
`delegated` var en tom tilstand.
Noterne er kommet til — listen med en note åben, og den samme note som kilde.
Fjorten billeder i alt.
WebP'en skrives af den browser, der i forvejen står åben, frem for af cwebp:
et program mere at installere hører ikke til i et script, hvis hele pointe er,
at der ikke er noget at installere. PNG'erne ligger nu kun i docs/ og WebP'en
kun i site/, hvilket er præcis dét, de to steder læser — før lagde et kørsel
tolv usporede PNG'er i site/, som ingen brugte.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Skærmbillederne skulle tages på engelsk, og det var dét, der fandt dem:
en konto sat til engelsk fik stadig "Forsinket" over de forsinkede opgaver,
"Uden sektion" over den første kolonne på tavlen, "Aldrig" i gentagelses-
menuen, "hver dag" på rækken, "til instansen · inviteret af" på invitationen,
"3 projekter" på kontoen, og hele historikken — nitten hændelser, alle på
dansk. En funktion, der kun virker på det ene sprog, er en funktion, der er i
stykker.
Gentagelsen er den eneste, der ikke bare manglede en nøgle. Serveren skriver
`recurrence_text` på dansk og skal blive ved med det: én besked går over
WebSocket til alle i projektet på én gang, og de læser ikke det samme sprog.
Så de regler, der har et navn, får det i when.js — på den side, der ved, hvis
skærm det er — og alt mere specifikt falder tilbage til serverens ord.
Mellemrummet i historikken er skrevet ud. Svelte fjerner det, der åbner en
blok, så "inviterede" og adressen løb sammen til ét ord.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Samme gitter som Kommende, givet en liste mere at tegne. Ikke et gitter nummer
to, og grunden er den del, der er nem at få næsten rigtig: et mål skal afgøre
ud fra `dataTransfer.types` alene, om det tager imod, og to kopier af det er to
steder at være næsten rigtig. En opgave trukket her flyttes præcis som alle
andre steder, fordi det er den samme kode, der gør det.
En begivenhed er ikke en opgave, og cellen siger det før et eneste ord er
læst: en flade af farve, hvor en opgave er et kort med en farvet kant. Den kan
ikke trækkes — forbindelsen er læsende, og en chip, der letter fra siden og så
ikke vil lande, læses som en fejl frem for som en regel — og den er ikke en
knap, for der er ikke noget at lukke ved en andens møde. Farven er Googles egen
for den kalender, så to kalendere i ét gitter kendes fra hinanden på den farve,
personen allerede kender dem på.
Farven tjekkes frem for at blive troet: den ender i en inline `style`, og en
værdi, der ikke er en farve, kan lukke erklæringen og åbne en anden. Linket
tegnes kun, hvis det begynder med `https://` — et href er dér, hvor en
`javascript:`-adresse bliver til et klik, der kører.
Prøven bruger uge-visningen med vilje: en månedscelle skæres af ved tre chips,
og resten af suiten lægger opgaver på andre dage. Datoen læses ud af siden, ikke
ud af testprocessen, som de øvrige har lært at gøre.
Samme registrering som Gmail, et andet scope og en anden redirect-URI. Google
udsteder en refresh-token pr. godkendelse og holder op til hundrede i live pr.
konto, hver med sine egne scopes — så en forbundet kalender rører ikke en
Gmail, der virker, og at afbryde den ene tager ikke den anden med.
Callbacken har sin egen sti. Én callback, der ud fra gemt tilstand afgjorde,
hvilket af de to flows der kom tilbage, ville lægge valget af *hvilket
token-lager der skrives* bag et opslag — og går det galt, går det galt ved at
overskrive en virkende Gmail-forbindelse med kalender-tokens.
Begivenhederne caches i et vindue på 90 dage tilbage og et år frem. Et
månedsgitter bladrer, og et kald til Google pr. piletast er en kalender,
der er langsommere end den kalender, den viser. Vinduet har en kant, og
endpointet siger hvor den er: et tomt svar uden forklaring er en kalender,
der lydløst påstår, at en dag er fri.
Spørgsmålet er ét: hvor mange bruger det her. Svaret er to værdier én gang i
døgnet — et tilfældigt id, installationen selv fandt på, og versionen. Ikke andet.
Beskeden er lille nok til at stå i fuld længde på indstillingssiden, og den gør
det, fordi et løfte om telemetri er præcis så meget værd som læserens mulighed
for at efterprøve det.
**Ikke obligatorisk, og grunden er praktisk frem for principiel:** det kan ikke
håndhæves. Domænet kan blokeres, binæren kan patches, og en fork fjerner den i
første commit — så obligatorisk køber intet over slået-til-som-udgangspunkt og
koster den tillid, resten af programmet arbejder for. Hemmeligheder forsegles
her, nøglen holdes uden for databasen, og sikkerhedskopisiden advarer om, hvad en
kopi indeholder. Telemetri, man ikke må sige nej til, er den ene sætning i den
historie, der ikke passer.
Uenigheden med `config.UpdateCheck`, som er slået fra med den modsatte
begrundelse, står skrevet i pakkens egen dokumentation frem for at blive glattet
ud. Den er reel, og læseren fortjener at se begge halvdele.
Id'et findes på og gemmes ved første brug frem for at blive udledt af værtsnavn
eller adresse. En hash af noget om maskinen ville være stabil, bekvem og
samtidig en værdi, modtageren kunne holde op mod et gæt — og det er forskellen
mellem anonym og pseudonym.
Modtagerens side er den samme binary med en kontakt: `POST /api/v1/beacon` uden
login, fordi den, der ringer, er en fremmed installation. Den svarer 404, når
instansen ikke er tælleren, så en almindelig installation ikke er en spamspand,
der svarer 200 på alt. Ratebegrænset som login-ruterne, og tabellen har et loft:
endepunktet indsætter en række på et id, den, der ringer, har fundet på, og uden
et loft kunne enhver fylde disken.
Tallet er "set inden for tredive dage" og ikke et løbende total. Et total stiger
kun og holder op med at betyde "hvor mange bruger det" og begynder at betyde
"hvor mange har prøvet det".
`internal/safedial` kom til undervejs: modtagerens adresse kan skrives i fladen,
og en adresse, en bruger angiver, må ikke kunne pege indad. Kontrollen ligger på
den opslåede adresse og ikke på URL'en, fordi et navn kan svare 127.0.0.1 — og
svare noget andet anden gang, efter en kontrol ved parsning er gået godt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Indført i aften, sammen med billedvisningen, og fundet ved at læse den igen.
`escapeHtml` tager &, < og > — ikke anførselstegn, og det er rigtigt for tekst
mellem to tags, hvor et anførselstegn ikke betyder noget. `alt` er ikke tekst
mellem to tags. Det står inde i en attribut, og der lukker det første
anførselstegn den, hvorefter resten står som markup:

blev til et <img> med en onerror på. Adressen var i forvejen bundet til vores
eget bilagsmønster, så den var ikke vejen ind — teksten ved siden af var.
Det er ikke en teoretisk vej. En note deles ved at ligge i et projekt, så den
læses af alle, der kan læse projektet, og scriptet ville køre på vores eget
domæne med sessionscookien, hos den der åbnede noten.
En værdi i en attribut har brug for sin egen undslipning, og har nu en. Prøven
skriver noten, åbner den og ser efter, at hændelsen står som tekst i alt og ikke
som en attribut på elementet — og at billedets 404, som er selve vejen en
onerror ville blive kaldt ad, ikke kalder noget.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fundet af prøverne, som for første gang kørte forbi midnat.
Dagen blev læst som `completed_at.slice(0, 10)` — de ti første tegn af et
tidsstempel, der er i UTC. Overskriften sammenlignede med *lokal* dato. I dansk
sommertid er de to ikke den samme dag i de første to timer efter midnat, så en
opgave lukket 00.30 blev sat under "I går", indtil klokken blev to. Tyve timer i
døgnet er de ens, hvilket er derfor prøven har været grøn hver eneste gang før.
Dagen udregnes nu lokalt, som den overskrift den skal svare til.
Og prøven for invitationslinket kunne kun bestå seks dage om ugen. Den ledte
efter "males inden fredag" som én tekst, men hurtig tilføjelse læser "fredag" som
forfaldsdatoen og tager ordet ud af titlen — tilbage står "males inden" plus et
mærke, hvis ord skifter med ugedagen. Kørt på en torsdag hedder mærket "I
morgen", og så fandt den ingenting. Prøven handler om invitationer, ikke om
datoer, så den ser nu på titlen alene.
Uddraget i notelisten viste `` overhovedet, så en note fra Apple Noter viste sin egen billedadresse som
tekst. Filen var hentet, gemt og hæftet på noten; det eneste, der manglede, var
at nogen tegnede den. Kun vores egen adresse bliver til et billede — et
`` fra en indsat tekst ville ellers hente fra en fremmed vært, når
noten åbnes, og en note er det sidste sted, man vil have en sporingspixel. Og
vejen tilbage skulle med: uden `IMG` i inlineToMarkdown ville den første gemning
have taget hvert billede ud af noten igen.
**Titlen blev revet i stykker.** Noter deler en linje op i så mange stumper, den
har brug for — "Aalborg Kommune" står som
`<h1>A</h1><h1>alb</h1><h1>org Kom</h1><h1>mune</h1>`, fordi der er en
attributkørsel bag hver stump — og eksporten læste et linjeskift ved hver af dem.
Noten kom ud som fire overskrifter oven på hinanden, altså ødelagt på den mest
synlige måde der findes: i sin egen titel. Samme rod gav `**M****øde**` for fed
og `*M**øde*` for kursiv, hvor de to stjerner i midten læses som fed. Nu brydes
der kun, når blokken skifter, og et mærke, der lukkes og straks åbnes igen, er
ingenting.
**Datoerne fandtes ikke.** Eksporten læste dem aldrig, og Markdown havde ingen
plads til dem — så en flytning af tolv hundrede noter blev en bunke, der alle var
skrevet samme aften. Rækkefølgen er halvdelen af, hvad et arkiv er. Nu står de i
et YAML-hoved, som Obsidian og Bear allerede skriver, og importen læser det.
`SetNoteTimes` frem for gennem SaveNote: `updated_at` betyder "hvornår skrev
*dette* program noten sidst", og lod man enhver kalder sætte den, ville kolonnen
betyde det, den sidste skriver havde lyst til.
**Og listen kan sorteres** — senest rørt, oprettet, navn. Favoritter står øverst
uanset hvad; det er hele meningen med at gøre en note til favorit.
Eksporten henter nu i klumper. Ét kald pr. note tog halvanden time for tolv
hundrede, ikke fordi Noter er langsom, men fordi hvert kald er en proces, der
skal startes — og det gjorde det umuligt at rette en fejl i oversættelsen og se
resultatet, for hver runde var en frokostpause. Antallet af bilag kom med i
klumpen af samme grund: ni ud af ti kald var en proces startet for at få svaret
nul.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Den var lagt fast øverst i ruden, med den begrundelse at en menu, der følger
markøren, rykker sig et par pixels for hvert bogstav. Det er rigtigt, og det var
alligevel forkert: i en note på en skærmfuld skriver man `#` nederst og får en
menu i toppen — fire hundrede pixels fra det, øjet ser på, og langt nok væk til
at man ikke opdager, at den er der.
Begge dele kan lade sig gøre. Positionen måles, når menuen åbner, og ikke igen
mens listen snævres ind: den står ved tegnet, og den står stille.
Målt på `#term` frem for på markøren, fordi et sammenfaldet område ikke har nogen
kasse på en tom linje. Klemt inden for ruden, og vendt op over linjen, når der
ikke er plads under — ellers ville en note, der er skrevet helt ned i bunden,
lægge menuen uden for skærmen.
Og pak-noter-i-portioner.py, fordi en Apple Noter-eksport er hundrede og
enoghalvfems megabyte, hvoraf teksten er fire: importen tager 64 MB ad gangen, og
tunnelen foran den tager mindre. Delt efter hvad en note vejer med sine billeder,
og altid med billederne i samme portion som den note, der peger på dem.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kildevisningen, fordi den rige flade ikke kan tage imod Markdown. Indsat i den
er "## Overskrift" fem tegn og ikke en overskrift, og der var ingen vej ind for
tekst skrevet et andet sted — hvilket er en mærkelig mangel i et program, hvis
hele lager er Markdown. Knappen </> viser filen, som den ligger i databasen, og
det, man skriver der, går tilbage gennem den samme oversætter som altid.
Fyldt fra kilden og ikke fra note.body, når man går tilbage: gemningen venter på
sin pause, så den vej ville kaste det nyeste væk. Og kildevisningen hører til den
note, den blev slået til på — skifter man note, er det den rige flade, man møder.
Margenerne. Teksten lå helt ude ved kanten, og på en bred skærm blev linjerne
hundrede og fyrre tegn — dér mister øjet hvilken linje det var på, når det
springer tilbage. Arket har nu marginer, der vokser med ruden, og teksten en
grænse på 46rem. Kun teksten: værktøjslinjen og foden hører til ruden.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seks ting fundet ved at bruge appen, i den rækkefølge de gjorde ondt.
En liste inde i en blok blev læst som tekst. htmlToMarkdown havde én gren for
ul og ol og lod alt andet falde ned i inlineToMarkdown, som samler efterkommere
til én streng — så et blokcitat med en liste i kom tilbage som "> PunktPunkt 2",
og en note fra Apple Noter, der er fuld af citater, mistede hvert punktmærke ved
den første gemning. Ingen havde rørt de linjer. Nu læses en blok, der rummer
blokke, igennem frem for flad, og et citat sætter "> " foran det, der kommer
indefra.
Indlejrede lister fandtes ikke. Den flade `list`-variabel kunne kun beskrive ét
niveau, så et indrykket punkt blev lagt ved siden af sit ophav. Nu en stak, og
underlisten lægges inde i sit <li> — det er den forskel, der kan læses tilbage.
En liste, der begyndte ved ti, begyndte ved ét bagefter. `start` skrives nu med
og læses med, så en gemning uden ændringer ikke gør noten om.
Tallet blev klippet af. Mærket sættes uden for tekstkanten, og 16px rakte til et
punkttegn og ikke til "10." — resten hang ud over kanten, hvor .page beskærer.
Renden er nu 2.2em og deles af begge slags, så punkter og tal flugter.
# viste seks vilkårlige projekter ud af tyve og så færdig ud. Loftet er væk,
listen kan rulle, og det, der begynder med det tastede, står øverst.
Og to ting om, hvordan det ser ud. Noteruden har fået sin egen flade, fordi et
ark, der holder op et sted, skiller det, man skriver, fra det program, man
skriver det i — `--surface` frem for `#fff`, så de fem temaer hver får deres.
Noter stod for sig selv over en streg, hvilket sagde det rigtige og kostede
halvfjerds pixels; den står nu i rækken med de faste visninger og bærer et ark i
stedet for en prik, hvilket siger det samme gratis.
Prøven skriver de fem listeformer, gemmer gennem den rigtige editor og læser
filen bagefter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rækken har nu titel i samme størrelse som en opgavetitel — en note og en opgave er
to ting af samme slags, og hver sin størrelse får den ene til at se mindre vigtig
ud. Under den står dato og begyndelse på samme linje, som Apple Noter gør det: to
linjer pr. note frem for tre, og datoen først, fordi det er den, man skanner efter.
Ligger noten i et projekt, står det nedenunder.
⌘K søgte kun i opgaver og projekter. Feltet ser ud til at kunne finde alt, så det
er svært at gætte, at halvdelen af programmet ikke var med. Og mens jeg var der:
"Projekt" og "Opgave" stod hårdkodet på dansk i paletten.
En note kan trækkes ind på et projekt i sidebjælken — samme mål som en opgave,
fordi det er samme gestus: læg denne her.
To fejl i søgefeltet, begge Safaris egen type="search":
Fokusringen tvang sin egen hjørneradius og tegnede en firkantet bue uden om en
rund kant — to streger, hvor der skulle være én. Den følger nu elementets form,
overalt i programmet. Og feltet skiftede udseende ved fokus: forstørrelsesglasset
forsvandt og pilleformen blev firkantet. Det tegnes nu af os.
Trækket og palettens noter er ikke fastholdt i en prøve. ⌘K, tastning og en
debounce oven på hinanden gjorde den upålidelig, og en flakkende prøve siger
mindre end ingen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Konverteringen læser nu Apple Noters HTML frem for at flade den ud med textutil.
Overskrifter, fed, kursiv, understreget, lister og monospace kommer med. En note er
ikke kun sine ord — et referat med overskrifter er ikke det samme referat som en
klump — og ti prøvenoter viste hvad det kostede.
Skrevet om til Python, fordi HTML skal læses og ikke gættes med regulære udtryk.
Den alvorligste fejl, prøvekørslen fandt: navn og krop blev hentet i hvert sit
kald, og Noter sorterer listen efter ændringsdato. Flyttede noget sig imellem de
to kald, fik en note én titel og en andens indhold. På ti noter er det synligt; på
tolv hundrede ville det være ubemærket rod. Der hentes nu ved id, og navn og krop
kommer i samme kald.
Sammenhængende monospace-linjer bliver til én blok. Hver linje i Noter er pakket
ind i sin egen div, hvis starttag tømmer den forrige — så mellem to tt-linjer kom
der altid en tom flush forbi, og hver linje i en terminaludskrift blev sin egen
blok.
Og i editoren ser en kodeblok ud som en terminal: mørk flade, monospace, sproget i
hjørnet, farver på strenge, tal, kommentarer, nøgleord og prompten. Monospace ER
kode i praksis, og en mørk flade siger det på afstand.
Farverne lægges på de blokke, markøren IKKE står i. At farve mens nogen skriver
betyder at bygge indholdet om ved hvert tastetryk og sætte markøren tilbage
bagefter — det virker ni gange ud af ti og flytter markøren midt i en sætning den
tiende.
Et API-token er ét ord, der er bredere end skærmen. Uden min-width: 0 skubbede en
sådan note hele listen bredere og klemte editoren sammen — på en note, man ikke
engang havde åben.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Du spurgte, hvad der sker med bilag og indsatte billeder. Svaret var: de forsvandt
stille. Scriptet konverterede til ren tekst, og en note fra Apple Noter, der kommer
over som ord alene, er ikke den note — den er et referat af den.
En note kan nu bære filer, i samme tabel som opgaver og grupper allerede bruger.
Filerne er indholdsadresserede og deduplikeres på disken, upload-vejen er skrevet
og afprøvet, og oprydningen af forældreløse filer går allerede den tabel igennem.
En tabel mere ville være et alt-sammen mere.
Importen læser arkivet i to omgange. Alt, der ikke er Markdown, gemmes først, så
en note, der bliver læst, allerede har adresser at få sine links rettet til —
noter og billeder ligger i den rækkefølge, arkivet nu har dem, og ét gennemløb
ville efterlade halvdelen af henvisningerne pegende på ingenting.
Kun de filer, noten faktisk nævner. Et arkiv er én mappe til mange noter, og at
hænge hvert billede på hver note ville gøre hundrede noter til ti tusind bilag.
Hentningen af en note-fil gav 404, fordi adgangskontrollen ikke kendte noter som
forælder. Et billede midt i den tekst, der navngav det, svarede altså "findes
ikke". Den følger nu noten, som følger sit projekt — samme kæde som alt andet.
AppleScriptet gemmer nu hver vedhæftning og skriver en henvisning til den i noten.
Vedhæftninger uden fil bag sig — links til websider, kort — springes over frem for
at vælte kørslen.
Migration 0021.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Det er løftet, hele designet blev lagt til rette omkring: noten på disken ER den
fil, man ville eksportere. Der konverteres ingenting på vejen ud, så der er
ingenting, konverteringen kan tage fejl af — filerne åbner i Obsidian, i en
teksteditor, i hvad som helst.
To ting, en eksport kan tabe stille, og som prøven holder fast:
Et skråstreg i en titel ville lave en mappe på Mac og Linux og blive afvist på
Windows. De tegn, ingen af de tre vil have, bliver til bindestreger.
Og to noter med samme titel er to noter — et arkiv med den samme post to gange er
et arkiv, der har mistet den ene. Den anden får et nummer.
Fejler skrivningen midtvejs, stopper den frem for at rydde op. Svaret er allerede
på vej ud, så der er ingen statuskode tilbage at sende: et afkortet zip siger
selv, at det er afkortet, hvilket er bedre end en komplet fil med en note færre.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Den anden vej af samme forbindelse. En note, der nævner en opgave, viste den; nu
viser opgaven noten. Uden det virker forbindelsen kun, hvis man tilfældigvis
sidder og læser noten, og det er den halvdel, ingen har brug for hjælp til.
Undervejs viste det sig, at en opgave slet ikke havde en adresse. Skuffen åbner
over den liste, man står på, og ændrer ingenting i URL'en — hvilket er rigtigt for
et klik og ubrugeligt for et link: man kan ikke sende nogen en opgave, og en note,
der nævner en, har ingen steder at sende dem hen. Min egen henvisningsform pegede
altså på noget, der ikke fandtes.
/opgave/{id} findes nu. Den åbner skuffen og træder til side — erstatter sig selv
med opgavens eget projekt, så man lander hvor opgaven bor, med den åben, og
tilbage-knappen ikke går gennem en side, der kun nogensinde viderestillede.
Prøven henter opgavens id fra API'et frem for fra adresselinjen, netop fordi den
ikke står der.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
De var det ikke. Nøglen blev gemt præcis som skrevet og slået op på samme måde,
så en note, der nævnte et projekt med lille begyndelsesbogstav, dukkede simpelthen
ikke op på det — stille, og uden noget der antydede hvorfor.
Foldet på vej ind og foldet i opslaget, så sammenligningen bliver ved med at være
en nøjagtig lighed og bruger sit indeks. Havde man i stedet sænket kolonnen i
forespørgslen, skulle hele tabellen læses.
Kun projekter. En [[notetitel]] er en titel, ikke en nøgle: to noter, der hedder
"Møde" og "møde", er to noter, og at folde dem sammen ville slå noget sammen,
nogen havde holdt adskilt. Der er en prøve for begge dele.
Migrationen samler de rækker, der allerede er skrevet. Primærnøglen er
(note_id, kind, target_id), så en note, der skrev begge stavemåder, ville kollidere
med sig selv — derfor bygges sættet forfra med INSERT OR IGNORE frem for en
opdatering, der ville fejle på parret.
Fladen viser projektets egen stavemåde og ikke den foldede nøgle. En etiket, ingen
har skrevet, og som står hverken i noten eller på projektet, ville være værre end
ingen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Titlen var udledt én gang og derefter ladt i fred, så den gik i stå i det øjeblik
nogen skrev begyndelsen om — en note med overskriften "Ny note" stod i listen som
"fdgfgfgh", hvilket var, hvad dens første linje havde været nogle rettelser
tidligere. Den udledes nu ved hver gemning, og ingen kan sætte den ved siden af
teksten: kolonnen er en kopi af kroppen, ikke et felt derudover. Feltet er væk fra
både HTTP-fladen og MCP-værktøjerne, for et felt, serveren ignorerer, er et løfte,
den ikke kan holde.
Skriver man #Kaf, foreslås #Kaffebaren. Ordet læses fra den tekstknude, markøren
står i, og ikke fra hele dokumentet — en note kan nævne tyve projekter, og kun det
under markøren er ved at blive skrevet. Listen ejer piletasterne og retur, mens
den er åben, som enhver anden fuldførelse.
Og under feltet står nu, hvad teksten forstår: # projekt, [[en anden note]], ⌘B,
⌘U. Samme idé som linjen under hurtig tilføjelse — en note er det andet sted i
programmet, hvor det, man skriver, bliver læst for betydning.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
En ny note åbner på sin titel, og linjen efter er brødtekst — uden at man vælger
noget. Det er det, Apple Notes gør, og det er også hvad listen har brug for:
første linje bliver notens navn, så en note, der begynder i brødtekst, hedder sin
egen første sætning.
To ting undervejs. Reserven for en tom note blev aldrig brugt, fordi en tom tekst
ikke gengives som ingenting men som ét tomt afsnit — den prøves nu på teksten og
ikke på resultatet. Og Chromium svarer på retur med et bart <div>, som læses som
brødtekst men ikke er formateret som det: afstanden mellem afsnit forsvandt
stille i det øjeblik, man trykkede retur frem for at vælge Brødtekst i menuen.
Listen viste desuden den rå Markdown i forhåndsvisningen — "# Møde med Anders" og
"**fed**" — hvilket er præcis det, editoren blev bygget for at holde op med at
vise. Den viser nu sætningen frem for notationen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
I dag, Kommende, Venter og Indbakken kan trækkes i den rækkefølge, man vil have
dem. På kontoen frem for i browseren: sidebjælkens bredde er en kendsgerning om
skærmen, og svaret på en telefon er ikke svaret på en skærm — men hvilken
rækkefølge man vil have dem i, er en kendsgerning om personen.
En liste af nøgler frem for et tal pr. visning. Ukendte nøgler droppes og
manglende føjes til sidst, så en visning kan tilføjes eller fjernes uden at
strande nogens rækkefølge — hvilket allerede er sket én gang, da Færdige
forlod menuen.
To ting, prøverne fandt undervejs, og begge var mine:
Rækken som trækgreb gjorde alt indeni trækbart, og sidebjælken er allerede
modtager for opgaver — en opgave trukket til I dag holdt op med at lande, fordi
rækken havde taget gestussen. Et lille greb tager ingenting, før man griber det.
Og drop-håndteringen slugte begivenheden, før den tjekkede, om den overhovedet
var en menuflytning.
Migration 0019.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fire udseender ved siden af de fem temaer: Verdande, Rolig (serif, mere luft),
Tæt (flere rækker, mindre luft) og Terminal (én bredde til det hele). De ganges ud
til tyve udseender fra otte små blokke CSS frem for at kræve en blok hver.
To akser, fordi det er to spørgsmål. Den, der vil have en serif om natten, skal
ikke opgive den mørke palet for at få den — og prøven holder netop det fast: at
vælge en skrift må ikke trække temaet med sig.
Udseendet sættes før første optegning, ligesom temaet. Ellers bliver siden sat op
i én skrift og derefter i en anden, og det er et synligt spring.
Det er også det ærlige svar på at ligne noget andet. Standarden er bevidst
genkendelig — det var det, der blev bedt om — men et program med ét muligt
udseende har lånt en form, det ikke kan give tilbage. Med udseender er ligheden en
indstilling blandt flere frem for det eneste, det kan være.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ikke en adgangsliste af sin egen. Projekter har allerede medlemmer, roller og
invitationer — alt sammen afprøvet, og alt sammen noget folk har lært her i
forvejen. En anden måde at give adgang på er et andet sted at gøre det forkert,
og de to ville være uenige den dag, den ene blev ændret.
To ting, prøven fandt, som begge var rigtige fejl.
Projektsiden viste kun noter, der NÆVNTE projektet, ikke dem, der lå i det — så
en note, man lige havde delt derind, dukkede ikke op. Nu vises begge slags.
Og noteoversigten viste kun dem uden projekt, så en note forsvandt fra listen i
det øjeblik, den blev delt. Det fik deling til at se ud som sletning. Listen viser
nu alt, man kan se: sine egne løse, og dem i de projekter, man er med i.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Man skriver nu i formateret tekst frem for i Markdown-kildekode. Titel,
Overskrift, Underrubrik, Brødtekst, Monotype, Blokcitat, punkt- og talliste, plus
fed, kursiv, understreget og gennemstreget — samme ordforråd som Apple Notes, for
det er det, en, der kommer derfra, allerede kan.
Filen under er stadig Markdown. Det er ikke et kompromis, nogen skal leve med i
stilhed: det er grunden til, at eksporten er til at stole på — filen, en note
bliver til, er den fil, den allerede var. Understregning er den ene undtagelse;
den findes ikke i noget Markdown-dialekt og gemmes som <u>, som enhver læser
sender uændret igennem. Det er bedre end at fjerne en stilart, kilden har.
contenteditable med execCommand. Den er forældet og også det eneste, alle browsere
er enige om; at skrive en markeringsmodel fra bunden er ugers arbejde og en ny
slags fejl netop dér, hvor tabt tekst er utilgiveligt.
Tre ting, prøverne fandt, som alle ville have kostet tekst:
Værktøjslinjen stjal markeringen, når man klikkede — knapperne tager ikke fokus
længere. Browseren skrev <span style="font-weight: normal"> for en ophævet fed,
hvilket en oversætter, der læser tags, taber; der bedes nu om tags, og styles
læses alligevel, fordi det ikke honoreres alle steder. Og markeringen med
Shift+Home tog hele dokumentet, ikke linjen.
Favoritter er stjernen på hver note. Noteoversigten har fået luft mod venstre
kant, og Gem og Slet er knapper frem for bare ord.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Formatering vises, mens man skriver. Overskrifter, fed, kursiv, kode, citater,
punkter, #tags og [[links]].
Et spejl bag et gennemsigtigt tekstfelt — samme greb som hurtig tilføjelse bruger
til at vise, hvad parseren forstod. En contenteditable ville lade teksten style
direkte og ville også ødelægge fortryd, autokorrektur og IME. Det er en dårlig
byttehandel i det ene sted i programmet, hvor nogen skriver en halv time i træk.
Mærkerne bliver stående, dæmpede. Obsidian skjuler dem uden for den linje,
markøren står i; at dæmpe dem altid er en mindre idé, der ikke flytter tekst under
markøren mens den bevæger sig — og den overlever mødet med et tekstfelt.
Overskrifter skifter vægt og farve, aldrig størrelse: en større skrift ville fylde
flere linjer end tekstfeltet gør, og så skred alt nedenunder.
Hele risikoen ved grebet er, at de to lag skal flugte tegn for tegn. Prøven måler
det frem for at stole på det: skrifttype, linjehøjde, ombrydning, polstring og den
faktiske højde af de to lag med samme tekst i.
Undervejs: kursiv-passet slugte " og " efter et fed-udtryk, fordi den løse stjerne
stadig var synlig for det. Hvert pas læser nu en kopi, hvor de tidligere pas'
tegn er blændet ud.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude kan nu skrive og søge i noter gennem MCP. Det er også svaret på Apple
Noter: der findes ingen eksport, der er værd at bruge, men en model, der kører på
maskinen, kan læse Notes.app og skrive det, den finder, ind herigennem — én note
ad gangen, med teksten som Markdown. En flytning, ingen skal bygge en parser til.
Værktøjet melder tilbage, hvilke links noten fik. En note, hvis #tag var stavet
forkert, ser ellers præcis ud som en, hvor det ramte, og i en chat ser ingen den
række, der blev skrevet.
Noter står nu for sig selv med en streg over. De er ikke en femte måde at se de
samme opgaver på, hvilket er hvad alt ovenfor er — de er den anden halvdel af
programmet, og i den række læste de som endnu et filter.
Færdige er ude af menuen og ligger under Indstillinger → Data ved siden af
papirkurven. Argumentet for at have den fremme var, at man ikke ved, hvilken
visning man lukkede noget i — det er sandt, og det er også et sjældent ærinde ved
siden af fire punkter, der læses hver dag. Den er der stadig, med en beskrivelse
af hvad den er til.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tredje etape. En liste til venstre, noten til højre — ikke et gitter af kort: en
note læses langt oftere på sin første linje end i sin helhed, og en liste viser
tyve af dem, hvor et gitter viser seks.
Gemmes når man holder op med at skrive. En note er det ene sted i programmet, hvor
en times skrivning kan gå tabt på ét fejltryk, og en Gem-knap er en knap, der
findes for at blive glemt. Teksten holdes adskilt fra den note, den kom fra, så en
gemning, der lander sent, ikke kan overskrive det, der er skrevet siden.
Og udbetalingen: skriver man #GarageRisteriet i en note, står den på projektets
side. Ingen har filet den derhen — den dukker op, fordi nogen nævnte projektet
mens de skrev. Slået op på navnet og ikke på et id, for et #tag skrives i hånden,
og et menneske skriver navnet.
Markdown i et almindeligt tekstfelt indtil videre. Live preview, hvor mærkerne kun
vises i den linje markøren står i, er næste stykke og det dyre; det her er den
form, den skal sidde inde i, og alt andet er allerede sandt uden den.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Første etape af noter. Tabel, CRUD, fuldtekstsøgning og de links, en note bærer.
Markdown i en kolonne, ikke rich text og ikke et format af vores eget. Eksport
var et krav, før der var skrevet en linje, og er den gemte form den eksporterede
form, er der ingen konvertering tilbage at tabe noget i — noten kan læses af
enhver anden editor den dag programmet ikke findes.
Et #tag i en note er DET SAMME tag som i en opgave. Ikke en anden slags, der
ligner. Det er hele svaret på "tag en note og se den i projektet": referencen er
den samme, så der ikke opstår to systemer, brugeren selv skal holde i sync.
Links gemmes som rækker, udledt af teksten ved hver gemning. Teksten er stadig
sandheden; tabellen er et indeks over den. Uden det ville "hvad peger på denne
opgave" betyde en gennemsøgning af al prosa, og den rude ville være for langsom
til at lade stå åben. De rives ned og bygges op frem for at blive sammenlignet:
en sammenligning er en anden beskrivelse af samme kendsgerning, og den kan komme
til at være uenig med teksten.
Noter ligger under projekter og arver deling, roller, papirkurv og historik. En
mappestruktur ved siden af ville være ét hierarki for meget.
MatchExpr fik navngivne kolonner. Den var skrevet til opgavernes og gav
"no such column" mod noternes — hvilket i det mindste er højlydt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>