ff0447317b0e308a892475228407d6ca8d250083
107
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3a02bc8304 |
fix(notes): søgningen svarede med det forkerte først, og et træk gjorde ingenting
Release / Build and publish image (push) Failing after 9m55s
**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> |
||
|
|
bdde9cc6a5 |
merge: Google Kalender som menupunkt, med Verdandes egen kalender lagt over
Release / Build and publish image (push) Failing after 9m58s
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> |
||
|
|
6b2cd0f259 |
merge: engelske skærmbilleder, og otte steder hvor fladen blev ved med at være dansk
Release / Build and publish image (push) Failing after 9m54s
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> |
||
|
|
3991ec9445 |
feat(notes): et billede kan indsættes og trækkes ind i en note
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> |
||
|
|
a5f2f769c4 |
fix(web): begivenhederne i en celle sorteres, hvor de tegnes
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. |
||
|
|
dce8800f93 |
fix(web): otte steder hvor grænsefladen blev ved med at være dansk
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> |
||
|
|
c3f2181efe |
feat(web): Kalender som menupunkt, med Googles begivenheder lagt over egne opgaver
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. |
||
|
|
2bd8e48aba |
fix(notes): en note med tre prikker i titlen blev smidt væk ved importen
Release / Build and publish image (push) Failing after 9m54s
`skipFile` afviste ethvert navn, der indeholdt "..", for at stoppe en zip i at skrive uden for sin egen mappe. Det er den rigtige idé rettet mod det forkerte: det beskriver ikke en sti, der klatrer ud af arkivet, men ethvert filnavn med to prikker ved siden af hinanden. En ellipse i en overskrift er almindelig, og noten "Så blev det endelig jul! Stay tuned... ☕️" blev derfor aldrig oprettet. Lydløst, oven i købet: filen blev sprunget over, før den blev talt, så svaret sagde det samme, som hvis den var kommet med. Én note ud af tolv hundrede, og den blev kun fundet ved at tælle efter, hvor mange der kom ud i den anden ende — 1140 hvor der skulle stå 1141. Nu læses ".." som et stiled, ikke som to tegn, og en Markdown-fil, filteret afviser, tælles som sprunget over. En import, der taber noget, skal sige det. Og move-ikonet i sidebjælken er væk. ⠿ sad i den to pixels brede rende til venstre for teksten og lå oven i rækkens egen kant — idéen var rigtig, pladsen var det ikke. Elementet bliver: det er dét, der ejer trækket, og gjorde man i stedet hele rækken draggable, ville alt indeni også blive det, og en opgave trukket hen på I dag holdt op med at lande. Nu er håndtaget usynligt og lidt bredere, og hånden ved hover er hele forklaringen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9b4e7686f9 |
feat(notes): datoerne følger med, billederne bliver tegnet, og listen kan sorteres
Release / Build and publish image (push) Failing after 9m54s
Fire ting, som en rigtig import af tolv hundrede noter fandt på en time. **Billederne blev vist som ord.** `markdownToHtml` havde ingen regel for `` 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> |
||
|
|
f6c6bdccbb |
fix(notes): projektmenuen står ved det # der bliver skrevet
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> |
||
|
|
9e687185fb |
feat(notes): vis noten som kode, og giv teksten en margen at ånde i
Release / Build and publish image (push) Failing after 9m59s
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> |
||
|
|
8229800767 |
fix(notes): lister overlevede ikke en gemning, og tallene blev klippet af
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> |
||
|
|
a4b12f3601 |
feat(notes): listen som Apple Noters, ⌘K finder noter, og de kan trækkes
Release / Build and publish image (push) Failing after 9m52s
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> |
||
|
|
43ad60a84a |
feat(notes): formateringen overlever, og monospace bliver til en terminal
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> |
||
|
|
6e83291444 |
feat(notes): opgaven viser de noter, der nævner den — og har fået en adresse
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>
|
||
|
|
5e670119db |
feat(notes): titlen er første linje, og # foreslår projektet
Release / Build and publish image (push) Failing after 9m59s
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> |
||
|
|
3223989a9d |
feat(notes): første linje er titlen, resten er brødtekst
Release / Build and publish image (push) Failing after 9m57s
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> |
||
|
|
f557d319ca |
feat(sidebar): de faste visninger kan sorteres, og rækkefølgen følger kontoen
Release / Build and publish image (push) Failing after 9m57s
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> |
||
|
|
1b02eecdab |
feat(notes): rich text, med Apple Notes' egne stilarter
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> |
||
|
|
5f04fe56bc |
feat(notes): note-værktøjer i MCP, og sidebjælken ryddet op
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> |
||
|
|
a2a39c4e17 |
feat(notes): en flade at skrive i, og noten dukker op i projektet
Release / Build and publish image (push) Failing after 9m55s
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> |
||
|
|
62498c2765 |
feat(sidebar): tallet ved et projekt er opgaver, og folden har en tast
"Hvorfor står der 2 ud for et tomt projekt?" Fordi tallet var member_count — hvor mange mennesker. Den samme grå kolonne betød tre ting: personer ved projekter, projekter ved grupper, opgaver ved etiketter. I en opgaveapp læses et tal ved siden af et projekt som opgaver, og det gjorde manden, der har skrevet appen. Nu er det åbne opgaver, og det vises når der er noget tilbage frem for når projektet er delt — som var den gamle betingelse, og grunden til at netop de tre delte havde et tal. Hvem der ellers er med står på projektets egen side. Tællingen hentes fra serveren igen, når opgaver flytter sig. Det ville være hurtigere at lægge til og trække fra lokalt, og det ville være forkert inden for en dag: en opgave flyttet mellem projekter, en lukket i en anden fane, en gentagende der lukker og åbner sig selv. Og sidebjælken kunne allerede foldes — der var bare kun en knap med et rektangel med en streg i, og det er ikke et ord. Nu gør ⌘B det samme, knappen siger det, og genvejssiden nævner det. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cbe66d75a5 |
feat(capture): skriv hvor som helst, og se hvad feltet forstår
Release / Build and publish image (push) Failing after 10m4s
To ting fra Doda. Begynder man at skrive et sted i appen, åbner hurtig tilføjelse med bogstavet allerede i feltet — ingen knap at ramme først, og tanken skal ikke overleve en rejse hen for at finde et sted at ligge. Og under feltet står nu, hvad parseren kan læse: #projekt, /sektion, @etiket, p1, en dato. Det stod i pladsholderen før, som er det ene sted en hjælp ikke kan stå: den forsvinder ved første anslag, altså præcis når den begynder at være nyttig. Prisen er de enkelte bogstaver som genveje. Ét tastatur kan ikke både stave "tal med Anders" og springe til I dag på samme t, og det, man gør halvtreds gange om dagen, er at fange. Navigation ligger i paletten, som kan mere og koster ét anslag. Genvejssiden er rettet, så den ikke lover q, t og u. Undervejs, i den rækkefølge testene fandt dem: reglen var først "er det et tekstfelt?", hvilket stjal bogstaver fra en fokuseret knap eller liste — den er nu "har noget som helst fokus?". Hjælpelinjen lå i flowet og skubbede hele listen ned, når feltet fik fokus, så klik landede en række forkert; tolv tests fangede det. Den svæver nu, og efter at have svævet ovenpå den første opgave og slugt dens klik tager den ikke længere imod pegeredskaber. Og en fejl, der allerede lå der: q slog feltet op på aria-label="Ny opgave", så genvejen gjorde stille ingenting for enhver, der kørte fladen på engelsk. Feltet findes nu på et mærke, ikke på en oversat tekst. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c151d6c98c |
feat(groups): omdøb med dobbeltklik, og slet der hvor man kan se hvad man sletter
Release / Build and publish image (push) Failing after 9m59s
De to ord "Omdøb" og "Slet" lå oven på gruppens navn, hver gang musen var i nærheden. De er væk fra sidebjælken. Omdøbning starter nu ved dobbeltklik på rækken — eller F2, hvis man har den i fokus og ikke vil bruge musen. Sletning er flyttet til gruppens egen side, hvor projekterne, den ikke tager med sig, står på skærmen ovenover knappen. Dobbeltklik på et navn, der også er et link, er ikke gratis. Første forsøg lod det første klik navigere, og så åbnede siden mens omdøbningsformularen kom frem — farveprikkerne flyttede sig ud under markøren, hvilket testen fangede som "element is not stable". Navigationen venter derfor to tiendedele på et muligt andet klik. Det er under grænsen for et dobbeltklik og over grænsen for at mærke det. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
bb25b94f96 |
fix(sidebar): en gruppe står nu hvor projekterne står, og hedder hele sit navn
To fejl i samme række. "Omdøb" og "Slet" lå i flowet og optog deres bredde også usynlige, så ARBEJDE blev tegnet som "ARBE"; de ligger nu over rækkens højre ende og dukker op ved hover. Og chevronen sad før navnet og skubbede en gruppe toogtyve pixels længere ind end de projekter, den indeholder — så en gruppe så ud til at ligge under dem. Den deler nu plads med gruppens prik: prikken hviler, chevronen tager over ved hover, og bliver stående mens gruppen er foldet. De sidste otte pixels sad i a-reglen, som giver hvert link i sidebjælken sin egen venstrepolstring oven i rækkens. Prøven, der holdt øje med rækken, målte at alt lå side om side — hvilket var netop den geometri, der skulle laves om. Den måler nu det, der blev bedt om: at navnet kan læses helt, og at gruppens prik står præcis hvor et projekts gør. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c4e480a0c4 |
fix(gmail): stop hiding why it failed, twice over
Release / Build and publish image (push) Failing after 10m6s
Reported from use: "Fetch now" said "something went wrong" and the error log was empty. Both halves were the same bug wearing two hats. `handleGmailSyncNow` answered 502 with the code `internal_error`, which the interface maps to a generic sentence — so Google's own words, which are the whole diagnosis, were replaced by one that says nothing. And `writeError` records nothing; only `s.internal` does. So there was no message to read and nowhere to look it up afterwards. `s.upstream()` is the answer: 502 because this is somebody else's service saying no rather than this server breaking, a code of its own so the interface can quote what was said, and a row in the error log — which is the entire reason that log exists. A Gmail sync failing quietly for a week is otherwise invisible: the background job writes a line the next restart erases. The same connection also left the mailbox unnamed. The address is learned at connect time and that call can fail; a warning went to the log and an empty string to the database, so the page said "an unknown account" and kept saying it. The only way back was to disconnect and reconnect, which nobody would guess. The status endpoint asks again when it finds none, so a wrong answer lasts until the next look rather than until somebody reconnects. Ten Danish strings were hiding in `confirm()` and `app.toast()` calls, including every destructive confirmation in the app — the worst possible set to leave in one language, since it is what somebody reads in the half-second before agreeing to lose something. The i18n guard reads markup and attributes, and a string argument lives in the script block. It reads those now, and found four my own grep missed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
19aa02e80c |
feat(projects): section headings you can count at a glance, and an Inbox that speaks the language
Two from use. Section headings were small uppercase text over a hairline — a heading you have to look for. A project with four sections is four groups, and the eye should be able to count them without reading a word, so the heading gets a ground of its own and the rows sit against the page. `--surface` rather than a tint of the accent: the colour on that page belongs to the project, and a section is not a second thing to colour. A step in lightness separates them in all five themes without anybody measuring anything. And the Inbox. The server names it at account creation, before anybody has seen a settings page, so it is the one project name nobody chose — and an account made in Danish went on saying "Indbakke" inside an English interface, which reads as a string somebody forgot to translate. It was. `projectName()` translates it while it still carries the name the server gave it; rename it and that is your name for it, in every language, like any other project. The rename field keeps the raw name, or editing a translated label would save the translation. The screenshot set gains the list view, which is where sections read as bands rather than as columns — and the shape most people actually work in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
fe52818ab6 |
feat(auth): passkeys, as a way in and as a second factor
A password and a TOTP code are both shared secrets: the server keeps something that can be stolen from it, and the person keeps something that can be talked out of them. A passkey is neither. The private half never leaves the device, and the signature is bound to the origin that asked for it — so a convincing copy of the sign-in page at another address gets a signature it cannot use. Logging in asks for no email. The device knows which account its key belongs to, which is not only convenience: it means the sign-in page cannot be used to find out who has an account here. A key that verified its owner is both factors at once and completes the login; one that only proved possession still meets the TOTP step, exactly as a password does. The relying party id is derived from VERDANDE_BASE_URL rather than configured separately — a second setting that has to agree with the first is one that will one day disagree, and the failure is a login nobody can complete. An IP address is refused up front: no browser accepts one, and without the check the server issues a challenge happily and the browser throws SecurityError long afterwards. The list endpoint reports whether this deployment can offer them at all, so the interface never shows a button that cannot work. A challenge is stored server-side, answerable exactly once, and taken rather than read — two requests racing the same one would both succeed, which is the shape of a replay. A sign count that has gone backwards means the credential was cloned and is refused rather than logged. The Go tests cover who may reach these endpoints and that challenges are spent. The ceremony itself needs an authenticator, so the smoke test drives Chromium's virtual one over CDP — the only thing that can tell whether the base64url conversions are right, since one wrong byte produces a signature that will not verify and reads as a broken key rather than a broken encoder. That test runs at localhost in its own context while the suite stays on the loopback address, because WebAuthn refuses an IP and resolving localhost is slow enough here to cost the suite minutes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a52ad9817f |
feat(tasks): undo a completion, and a place to find what was closed
Reported from use: "I just closed a task by accident, and there is no undo and no list of closed tasks." Both halves were true. Completing is one click on a small circle beside a row you were only reading, and it takes the row off the screen — so the mistake and the evidence of it leave together. There was no name left to search for and nowhere to look: the project page can show its own closed tasks, but only if you already know which project it was in. The toast covers the mistake you notice at once. It names the task, so you can see which one you hit, and stands for twelve seconds rather than five — five is enough to read "could not save" and not enough to notice, decide and reach the mouse. A toast with an action also gets two targets: the message dismisses and the action acts, which were one button before and would otherwise put "yes, put it back" and "go away" in the same place. `/faerdige` covers the one you notice tomorrow. A place rather than a toggle in every view, because somebody hunting for a task they closed does not know which view they closed it in. Newest first — a record of what is finished is read backwards from now, not by a due date the task no longer has — and grouped by the day, because "I dag" and "I går" is how anybody would describe it. A recurring task is offered no undo. Completing it moves it to its next date rather than closing it, so there is nothing to reverse, and an undo that quietly rolled the date back would be a different act from the one it claims to be. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0dbf7a40bf |
feat(tasks): write a task straight into a section
The box at the top puts things in the project with no section, which is right for capturing. But a project has sections because the work belongs somewhere, and dragging every new task down into the right one is a second gesture for something you already knew when you typed it. Two ways in, because they answer different moments. A field at the foot of each section and each board column — standing there is enough, no syntax to know. And `/section` in quick add, for the box at the top and for Today and Upcoming, where you are not standing in a section at all. The field goes through quick add rather than createTask, so a line written there still parses its dates, priorities and labels. The sigil needed care: '#' and '@' never turn up in a Danish task line by accident and '/' turns up constantly. It has to follow whitespace and be followed immediately by the name, which "km/t", "3/4" and "20 kr / stk" all fail. The bare form is greedy, because "Kunder/Ordrer" is a real section name and the sigil is inside it. A section id from the client is checked against the project the task actually landed in. A `#project` in the same line can move the task elsewhere, and the id would otherwise follow it into a project that section does not belong to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d6d5bf590b |
fix(sections): a section can hold more than one task
Reported as "sections can only have one task", and that is exactly what it looked like. TaskList read the dragged task from its own `draggingId`, which is only set when the drag *started* in that list. A task dragged in from the unsectioned rows above, or from another section, arrived as null and the handler returned early — after it had already called `stopPropagation`, so the section around it never got the drop either. So a section could only be dropped into while it was empty, because an empty one has no row in the way to swallow the drop. Put one task in and there was no route to a second. The id comes out of `dataTransfer` at drop time now, and `dragover` decides with `carries()` rather than local state — which is the only thing a target can read while a drag is in the air. The stuck frame was the same cause: the section's own cleanup never ran, so the highlight stayed lit after the drop. It is cleared on `dragend` now, which fires once per drag on the source and bubbles to the window — the one place that is true however a drag ends, including one abandoned with Escape, which produces no drop at all and left the frame standing too. The smoke test drops a second task onto the row already in the section, which is the gesture that did nothing, and runs in WebKit as well. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4d8b70918b |
feat(i18n): the interface speaks both languages
The account has had a `locale` since the beginning and it only chose which grammar quick add read a line with — so picking English changed nothing anybody could see, and the field said so honestly: "Sprog i hurtig tilføjelse". It says "Sprog" now, because it means it. Every component goes through `t()`. Danish is the source: a key missing from English falls through to Danish rather than to the key, because a Danish sentence in an English screen can be read and `settings.notifications.title` cannot. `tag()` is the single place `Intl` is asked. Before, `'da-DK'` was written into a dozen components, and that is the half of a translation that gets left behind — an English interface still saying "24. aug." is exactly the shape of a job half done. There is a smoke test that switches to English and refuses to see a Danish month name in the calendar's heading. Colour and theme names are keys now. `colors.js` keeps its ids untouched — they are what the database stores, and `internal/store` reads them out of that file — but `name` is `'color.tomato'`, resolved where it is drawn. The Inbox keeps its name. It is a project row the server creates at signup, not a string in the interface, and it can be renamed like any other project. Four copies of "3 hours ago" became one, in `web/src/lib/when.js`. They had already drifted: one said "aldrig logget ind", another "aldrig", and two stopped at days where the others carried on to a date. Routes stay Danish. `/indstillinger`, `/projekt`, `/uddelegeret` are bookmarks and they are in every smoke test; translating them buys nothing and costs both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e3c7fc792a |
feat(i18n): the machinery, and the shell translated
The account has had a `locale` since the beginning, and it only chose which
grammar quick add parsed a line with — so setting it to English changed nothing
anybody could see. This is the other half of that field, started.
`t()` reads `i18n.locale`, which is `$state`, so every component that calls it
redraws when the language changes; that is what makes the setting take effect
without a reload, and why the module is `.svelte.js`. Danish is the source and the
fallback: a key missing from English falls through to Danish rather than to the
key, because a Danish sentence in an English screen can be read and
`settings.notifications.title` cannot. `tag()` is the single place `Intl` is
asked, so dates follow the language instead of being `da-DK` in a dozen files.
`internal/i18n` is a Go package that exists only to fail the build when the two
dictionaries drift — the same trick `colors_test.go` plays on the palette. It
checks the placeholders as well as the keys: a translation that spells `{n}` as
`{antal}` leaves `{antal}` on the screen, which is worse than an untranslated
sentence because it looks like a bug in the app rather than a gap in a dictionary.
Converted so far: the shell. Sidebar, layout, quick add, task row, command
palette — fifty-two keys, and the surfaces people look at constantly. Twenty
files remain, the settings pages being the heavy ones.
The language picker is deliberately not wired up yet. It still says "Sprog i
hurtig tilføjelse" and still only changes the parser. Wiring it before the sweep
is done would hand anybody who picks English a bilingual screen — the
half-finished-theme failure `app.css` already taught us. As it stands Danish is
unchanged and English is not offered, which is the honest state for something
half done.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
804963b222 |
feat(groups): a group becomes somewhere you can go
It was a heading over some rows: a name, a colour, and whether it was folded.
Enough to tidy a list, and not enough to be what people mean by it — "Arbejde" is
a body of work with context of its own, and documents that belong to all of it
rather than to any one project inside it.
`/gruppe/{id}` carries three things a heading cannot. The projects in it, each
with what is still open — open tasks, not all of them, because "12" beside a
project somebody finished last year is a number that means nothing. A description,
written in place and saved on blur, the same shape a task's description has. And
the files that govern the whole group: the contract, the drawing, the agreement
everything else rests on.
The name in the sidebar is a link to it and the chevron beside it still folds.
Two intentions, two targets — the single one they used to share could only ever
serve the smaller.
Migration 0012 rebuilds `attachments`, because its CHECK spelled out exactly which
parent columns may be set and SQLite cannot alter a CHECK. The rule is still
"exactly one parent", counted rather than written out as pairwise comparisons:
with three columns that is three clauses, with four it is six, and the version
that is wrong looks exactly like the one that is right.
A group's file is reachable by its owner and nobody else. A group belongs to one
person and is never shared, so `reachAttachment` asks about ownership rather than
a project role — both old callers resolved straight to a task, so a group's
attachment answered 404 for the person who uploaded it.
Three tests also stopped racing their own navigation. Creating a project navigates
and `goto` is a promise nobody was holding, so a quick add fired straight after
could land in the Inbox instead — intermittent, and it read as the quick add
having failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
eaff890860 |
fix(forms): Safari focused none of them, and a board could not make a section
Two reports, one sentence between them: "sections have no function, because you cannot create more than one." `autofocus` does not work in Safari for anything inserted after the page has loaded, and every inline form here is inserted after the page has loaded — rename a project, add a section, name a group. In Safari they all opened with no cursor in them and what you typed went nowhere. That does not look like a bug. It looks like the feature does not work. `use:focusOnMount` replaces all seventeen. And the board had no way to add a section at all. The button lived only in the list view, which is the one place nobody looks for it: a column on a board *is* a section, so that is where you go to make one. Chasing this turned up a third. The task drawer's three fields save on blur, and closing the drawer *is* a blur — the input is being unmounted and the browser fires blur on the way out. If the task left the store in the same moment, `task` was already null and the handler threw `Cannot read properties of null`, stopping the page. It only became reproducible once focus actually worked. Fields are 16px on a touch device now, whatever the component asked for. Below 16px, iOS Safari zooms in when a field takes focus and does not zoom back out — so the whole interface is left too large and has to be pinched back by hand, once per field. The type scale tops out at 15px, so every input in the app did it. The sidebar's resize handle sat at `right: -3px`. An absolutely positioned child counts towards its container's scrollable overflow, and `overflow-y: auto` resolves the other axis to `auto` too — so those three pixels gave the whole menu a horizontal scrollbar on every desktop, always. The account name could do it as well: `overflow: hidden` with no `min-width: 0` cannot shrink, so the row grew instead of ellipsing. There is a WebKit pass in the suite now, over one test. The rest stays Chromium on purpose — a smoke test exists to catch a build that does not work, and three engines find the same bug three times — but focus is the exception, and it took a person using the app to find that out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
04a7ba1024 |
feat(sidebar): headings that fold, a group head that is not indented, and colour where it is edited
Three things reported from use, all in the sidebar. Projekter, Delt med mig, Filtre and Etiketter fold, the way a project group already does. Kept on the account rather than in localStorage, and for the same reason the group's own `collapsed` is: folding a heading says "this is the part of my work I am not in right now", and that is as true on the laptop as on the desktop. The sidebar's *width* is the opposite case and stays per browser. A JSON array of keys rather than a column each, so a renamed heading is not a migration. The group heading is no longer indented — only what is filed under it. The colour dot sat as a sibling before the fold button and pushed the whole row in, and a heading that starts further in than the rows above it says the heading is inside something. And the colour is chosen while renaming. The dot was itself the button, which gave the heading four controls on a line narrow enough to crowd the name; both are edits to the same thing, so they belong in the same moment. The form closes on `focusout` and only when focus has left it entirely, and the swatches refuse focus on mousedown — Safari and Firefox do not focus a button on click, so `relatedTarget` would be null and the form would shut under the pointer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b169591209 |
feat(upcoming): a week at a time, so a drag can cross a month boundary
A month grid is anchored to a month, so the two days on either side of its edge sit in different grids — and you cannot page mid-drag, because a drag in flight swallows the click that would page. A week that straddles the 31st and the 1st has both days in the same row. The same component as the month, with `span="week"`, rather than a second one: the part that is easy to get almost right is the same in both. A drop target has to decide from `dataTransfer.types` alone whether it accepts, and two copies of that are two places to be almost right. Seven cells instead of forty-two, so the height a month spends on six rows goes into one and the cells hold a real list rather than a hint. On a phone the row stacks and each day carries its own weekday name, because a column of bare numbers is not a week. `calendar` still means the month in localStorage. It is the value already stored for everybody who chose it, and renaming it would silently reset them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
494640c1a8 |
fix(mobile): the drawer no longer pushes the page sideways
Release / Build and publish image (push) Failing after 10m8s
The task drawer was `width: 100vw`. A viewport unit includes the scrollbar gutter and a percentage does not, so the drawer came out a few pixels wider than the page it was pinned to and the whole document could be shoved sideways. It is `100%` now, and at phone width it is stated as four insets instead — a box defined by the viewport's own edges cannot be a rounding error wider than it. The scrolling body gets `min-width: 0` and `overflow-x: hidden` too, so a pasted URL or a long unbroken word in a comment cannot stretch the flex column and take the page with it. Writing the test for that found another one: creating a project from the sidebar navigated to it and left the drawer covering the screen. Every link in there closes it on the way out; the form did not, because it calls `goto` directly. Now it does the same thing for the same reason. The phone test checks the document cannot be scrolled horizontally, both with the drawer shut and with it open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7094e05ddd |
feat(sidebar): drop a task on "I dag"
"Do it today" is the most-made rescheduling there is, and the sidebar is where the pointer already is when you have picked a task up. Only that one of the two date views. "Kommende" is a range rather than a day — it starts today and runs a week — so a drop on it would have to invent a date its own label does not name. The month grid inside it takes a drop per cell, which is where a specific day is actually on offer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
35d9e3bdac |
feat: an assignee on the row, and the activity log made visible
Release / Build and publish image (push) Failing after 10m2s
Two more things the database has been recording and the interface has
never shown.
**Who a task is waiting on, on the row.** Only when it is not you: your own
list would otherwise carry your own face on every line, which is a column
of one initial telling you nothing. The point of showing an assignee is
that it is somebody else.
That needed names, and a task carries only an `assignee_id`. `GET /people`
is the primitive: everybody you share a project with, plus yourself,
fetched once per session. Not per row, and not per project either — a list
would have to know which project each task belongs to before it could ask
for that project's members. Deliberately *not* the instance's user list,
which is `/users` and administrators only: a task list has no business
enumerating everybody with an account here.
It sits first in the meta line, because who changes what the rest of the
line means — a date on somebody else's task is their deadline, not yours.
**And the activity log.** Nineteen kinds of entry have been written since
the beginning — every project, section, member, task and comment event —
and `GET /projects/{id}/activity` has answered honestly the whole time with
nothing on the other end. There is a Historik panel on the project now,
loaded when it is opened rather than with the page, and the event keys
become Danish in exactly one place. An unknown key falls through to itself
rather than being hidden: a build that records something this table has not
learned should still show that it happened.
One test assertion had to change, and it is worth saying why. It matched
"males inden fredag" against a row whose title is "males inden" — quick add
read "fredag" as the due date — so it was really matching the title
concatenated with the metadata beside it. Putting the assignee between them
broke it. The assertion now names the task's own text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
fc32fea607 |
fix(projects): a section can be dropped into, and a failed load says so
Release / Build and publish image (push) Failing after 9m53s
Two things from the same report — an external member who could see a project but could not get work into it. **A section with no tasks could not be filled by dragging.** Only the task rows accepted a drop, so the target was always another row; a section with none had nothing to aim at, and a section you have just made is empty by definition, which is exactly when you want to put something in it. The board has taken a drop on the column since it was written. The list now does the same: the section is the target, and a drop on it puts the task at the end. Dropping on a row still means "here exactly" and stops there rather than bubbling to the section behind it. The unsectioned block at the top is a target too, which is how a task comes back *out* of a section — previously only possible if some other loose task happened to be there to aim at. **And the page said "Projektet findes ikke, eller du har ikke adgang til det" for three different things.** `project` started null with no loading state, so that sentence was what you saw before the first request returned, what you saw when it failed, and what you saw when you genuinely had no access — and the effect never asked again, so the wrong one stayed until somebody reloaded. That is very likely what was reported: the container was restarted twice while people had the app open, and a request that lands mid-restart leaves the page insisting the project does not exist. Four states now. Loading renders nothing, as the shell does. 404 keeps the old sentence, since the server deliberately answers 404 for both "no such project" and "not yours" and the interface must not guess between them. Anything else says the request failed and offers to try again. What is *not* a bug: a viewer cannot create or move tasks. Checked at the API — an editor invited by link gets 201 on create, 201 on quick add and 200 on a move into a section; a viewer gets 404 on all of them, which is the rule working. If the person reporting this cannot add tasks, their membership is viewer rather than editor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
550f420c59 |
feat(sidebar): colours you can choose, and projects indented under their group
`projects.color` has been in the schema since the first migration, has defaulted to 'graphite' on every row ever written, and nothing has ever read it. There was no palette, no name-to-colour mapping, and the dot in the sidebar was hardcoded to a border grey. So this is not "add a picker"; it is implementing colour for the first time. Names, not hex values. A hex in ten thousand rows is a decision nobody can revisit; a name is resolved when the interface paints and can be retuned for a new theme or a new display. It is also the difference between a database that stores a fact and one that stores a rendering. The set is closed and refused at the edge — an unknown name would be stored happily and painted as the default, which looks like a colour that did not save. One palette across all five themes, and this is the one place in app.css that is not redefined per theme. Everything else there is text, a ground or a border and has to be measured against what sits behind it. These are small solid marks beside a label that already carries the meaning: what they owe is to be told apart, and to be visible on both a near-black and a white ground, which is why all ten sit in the mid-tone band. It also keeps them out of the trap the themes are in — a colour added to four blocks and forgotten in the fifth looks finished and falls back silently. There is no fifth block to forget. The list is written down three times: Go for validation, colors.js for the picker, app.css for the tokens that paint. So a test reads the other two — the same small-scanner approach openapi_test.go takes to the spec — and fails if the server accepts a colour nothing can draw, or the picker offers one the server refuses. Groups get a colour of their own (migration 0006) and projects inside a group are indented. The indent is to the group's *name*, not to its dot: the eye follows the text, and lining the projects up under the dot leaves them looking a step short of where the heading starts. The drop line is indented with them, so a drop inside a group reads as landing in the group. The group's dot is the control as well as the preview — you press the thing you are about to change, as on the project page. That was not the first design: a third text action, "Farve", sat beside Omdøb and Slet, and those carry `opacity: 0` rather than `display: none`, so they take their width whether or not you can see them. A group called "Arbejde" rendered as "ARBEJD". Moving the control onto the dot gave the name its room back. The smoke test measures the indent at the content edge rather than the element's, because the indent is padding and the box does not move — which is how the first version of that assertion passed 12 against 12. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a8b453843d |
fix(sidebar): Omdøb og Slet lå oven i hinanden i et gruppehoved
Release / Build and publish image (push) Failing after 9m53s
The "+" beside "Projekter" is a 20px square icon button, and the rule that makes it one was written as `.group-head > button`. A group's heading is also a `.group-head`, and it has two buttons in it that are words rather than icons — so Omdøb and Slet were each squeezed into a 20px box, where they overflowed and were drawn on top of each other. They still worked. Every assertion in the smoke test clicks by accessible name, which is unaffected by a button being the wrong size, so the whole suite passed on a heading that read "0 Om…Slet". It took a screenshot from somebody actually using it. The selector now asks for the class instead of for "a button in a heading", so the next text button added there is safe by default rather than by remembering. And the smoke test measures the row: each button must start at or after the previous one's right edge. A layout assertion is unusual here on purpose — the rest of the file deliberately tests behaviour — but this is a failure mode behaviour cannot see. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6b6b838167 |
feat(views): "Venter på andre", and a project you share stays yours
`assignee_id` has been on tasks since sharing was built and delegating has been possible from the drawer since yesterday. What was missing was anywhere to see it: work handed to somebody else left your lists and appeared in nothing. GET /delegated returns everything assigned to somebody who is not you, in the projects you can see, grouped by the person. Grouped on the server rather than flat, because the question is about people — "what is Anders sitting on" — and a client cannot group it itself: it would need the name behind an assignee id from a project it does not happen to have open, and there is no way to ask for that. The names come back with the tasks, so one request is one screen. The filter is a field of its own rather than a sentinel in AssigneeID. "What is on my plate" and "what have I handed over" are different questions, and one string that means an id most of the time and a magic word the rest is a field somebody eventually passes a real id to. An assignee whose account is gone is named "Ukendt" rather than dropped. Losing the row would make work disappear from the one view that exists to stop work disappearing. The sidebar entry only appears where delegating is possible at all. On an instance with one person it could never have anything in it, and a permanent empty entry is a question the sidebar keeps asking and answering "no". Building it turned up a bug in the split above it. "Projekter" and "Delt med mig" were divided on `shared`, which only means the project has more than one member — so inviting somebody to a project of your own moved it under "Delt med mig", where it lost its place in the order, stopped being draggable, and could not be filed under a group. It is divided on ownership now, which is what the two headings have always said. Sharing your own project is not the same event as somebody sharing theirs with you. The smoke test now follows the whole loop in one go: invite, accept in a second browser, assign a task to the person who just joined, and find it under their name — with the shared project still under "Projekter" on the way past. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
34b718529c |
fix(auth): the two links sent by email now go somewhere
An invite has always emailed `/invite?token=…` and "Glemt adgangskode?" has always emailed `/reset?token=…`. Neither address existed. The Go binary serves the app shell for anything it does not recognise, the shell found no session, and both links opened the plain sign-in form — asking an invited person to log into an account that did not exist yet, and asking somebody who had forgotten their password for their password. Both APIs were finished. `POST /auth/signup` takes the invite token, reads the email address off the invite rather than the form — so a link cannot be redirected to somebody else — creates the account, accepts the invite and starts the session, all in one transaction. `POST /auth/password/reset` takes the reset token and ends every session the account had. Nothing was missing but two pages. They are branches of SignIn rather than screens of their own, which is what that component already says it is for: one decision tree, so a person is never redirected mid-authentication. The routes exist because a link has to be an address, and each renders the same component; it reads the token out of the URL itself. The reset does not sign you in afterwards. The whole point of ending every session is that somebody may have been in the account, and quietly opening a new one here would undo the half of that which matters. Two states that are easy to leave out, and both are reachable in one click from an old email: a link with no token says so instead of showing a form that cannot succeed, and a link opened while already signed in as somebody else offers to log out rather than creating a second account for a person who is right there in the first one. The smoke test walks the whole invite: share a project, read the link out of the panel that shows it when there is no mail server, open it in a second browser context, create the account, and find the shared project in the sidebar. A second context and not a second page — inheriting the session would test the one case that never happens, which is what it did on the first run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c14559ed0c |
feat: project groups, a month on Kommende, and dragging things where they go
The three the handoff named as next. They are one commit because they are one mechanism: all three end in something being dragged, and a drop target cannot tell what is coming without the module they now share. Groups are foldable headings over the projects in the sidebar. A table rather than a `group_name` column on the project, because a group has to be renamable in one write, has to survive being emptied, and has to remember whether it is folded — none of which a string repeated on every project row can do. Folded state lives on the row, not in localStorage. The sidebar's *width* is a property of the screen you are sitting at, which is why that one is per browser; a folded group is a statement about the group — "this is the part of my work I am not in right now" — and it is true on the laptop and on the desktop. There is nobody else's answer to disagree with either: a group belongs to exactly one person. A shared project cannot be filed. `group_id` is a column on the project row, so a member grouping somebody else's project would move it in the owner's sidebar too — the same reason `sort_order` is owner-scoped. The listing reports the column only to the owner, so a viewer is never told where somebody else keeps it. Deleting a group takes the heading and leaves the projects, and the sidebar decides where a project renders by whether its group still exists rather than by the id alone. A group deleted in another tab arrives as one event and not one per project, so for a moment the projects still name a heading that is no longer drawn — filing by the raw id would drop them out of the sidebar entirely. Kommende gains the month grid the project pages already had, behind a Liste/Kalender toggle. The two views ask for different things — seven days from today against six whole weeks around an arbitrary month — so each loads its own, and the grid reports which dates it is showing rather than having the page guess. Which view is remembered in localStorage: a month of seven columns needs room, and the answer on a phone is not the answer on a wide monitor. And a task can now be dropped on a project in the sidebar, on a day in Kommende, or on a cell in any month grid. Onto a project it is a *move*, not a field: `sort_order` belongs to the project, so it has to find a place among that project's tasks and leave its old section behind. Each drag now carries its own MIME type. `getData` is deliberately unreadable while a drag is in the air — the browser hands the payload over on drop and not before, so a page cannot read what is being dragged across it — and `dataTransfer.types` is all a target has to decide with. Deciding is the point: calling preventDefault on a drag you cannot accept is what turns "no" into a silent, wrong drop. The sidebar takes a project and a task on the same row and has to tell them apart. Two things fell out of building it. The month grid has always had one cell narrower than the other six: `.today` styled the "I dag" button in the header, and the day cell for today wears the same class, so `margin-left: auto` was shrinking a grid item to its content. It is scoped to the header now. And a project page listed whatever was in the store rather than what belonged to that project, which did not matter until a task could leave a project while you were looking at it. Covered by eight new API tests and two new smoke tests, and the smoke tests drive the drags for real — the type check they exercise exists only while a drag is in flight, so nothing else can reach it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f15c440942 |
feat(realtime): keep projects and labels current too, not only tasks
The socket carried task events and nothing else, so a project or a label
created anywhere — another tab, another device, Claude through the
connector — did not appear until a reload. Tasks updated live and the
sidebar did not, which reads as the sidebar being broken rather than as a
gap in what is published.
Projects and labels go to the *user* rather than to a project. A project
that has just been created has no subscribers, because nobody was
watching a thing that did not exist a moment ago.
Labels are announced as "something changed" and re-read, rather than sent
as a payload. The sidebar wants each label's task count, which no single
event carries and which moves whenever a task gains or loses a label —
including through quick add, where "@regnskab" invents the label inside a
task write and no label event is sent at all. Comparing the label lists
on a task change is what catches that, without re-reading on every ticked
checkbox.
And projects now go through an upsert, because the same row arrives twice
— once as the response to the request that created it, once over the
socket. Appending both puts two ids into a keyed {#each}, which throws
and stops the sidebar rendering. The tasks already had that guard; adding
the project event without it reintroduced the identical bug, and the
smoke test caught it on the first run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
3b521b463c |
feat(projects): add, rename and delete sections
The board has drawn a column per section since it was written and there was no way to make one. Sections could only be created by the Todoist importer or a template — so an ordinary project could never have a heading in it at all. Add, rename and delete, in the list view where the headings already are. The controls appear on hover rather than sitting beside the heading permanently: a heading with two buttons always next to it stops reading as a heading. Deleting says how many tasks are in the section and that they will survive it, because the API keeps them and drops their section — which is the right behaviour and not what "delete section" sounds like. The list is re-read afterwards, since those tasks move to the unsectioned group and nothing else would tell the view they had. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9056c44c9a |
feat(tasks): move, repeat, delegate and re-read a task from its drawer
Four things you could not do to a task you had open, and one that had never worked at all. **Project, recurrence and assignee** are now fields in the drawer. A task created in the wrong place could only be fixed by dragging it, a repeat could only be set by typing it into the title when the task was created, and delegation had no interface whatsoever despite assignee_id being in the API from the start. The assignee only appears on a project with somebody else in it, because on your own there is nobody to choose. **The title is read the way quick add reads it.** Typing "i morgen kl 14 p1" into a title used to leave it as those words, so where you typed a sentence decided what it meant. The parse runs on the server — the grammar is Danish and English and lives there — and applies only when it consumed something. What it took is said out loud, and the fields on screen are updated to match: the drawer otherwise seeds its inputs once per task so a save cannot overwrite what you are typing, and that rule has to bend exactly here, where the save changed the very fields in front of you. **And the member list has never worked.** The share panel, and now the assignee field, ask for it. Every call errored: the ORDER BY named an expression, and SQLite only accepts result-set columns across a UNION, so it failed at query time with "1st ORDER BY term does not match any column in the result set". Found by the smoke test that opens a task, which is the second real bug that suite has caught by watching for failed requests rather than only for what renders. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8c748c2a88 |
feat(sidebar): drag it wider, fold it away, and have it stay that way
The sidebar was 268px and always there. On a wide monitor that wastes the room; on a laptop it takes room the list needs. The width drags from a handle on the border — 7px of target over a 1px line, because a 1px target is a target you miss — and the whole column folds away from a button in the topbar. Both are remembered. In localStorage rather than on the account: this is a property of the screen you are sitting at, not of you. The same person on a laptop and a wide monitor wants different answers, and syncing it would make one of them wrong every time they switch. The handle is also a slider for the keyboard. Arrow keys move it, Home puts it back, and so does a double-click — a drag handle with no keyboard equivalent is a setting some people simply cannot reach. Folding is desktop-only, and the media query says so rather than leaving it to chance: below the breakpoint the sidebar is a drawer that opens over the page, and a collapsed drawer would open to nothing. Clamped to 200–480px in the store, so a stored value from an older build cannot make it unusable either. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |