fa816b2e41dda10297329a684e663f6a3e150727
49
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
de95df44c1 |
fix(notes): a [[note]] link is one you can follow
Picking a note from the [[ completion wrote [[Title]] and the server made a real link from it — but the link only ever showed in the panel under the note as plain text. It said the note pointed somewhere and gave no way to go there, which reads as "it didn't become a link at all". The links in that panel are buttons now. A [[note]] resolves by its title to the note — the one already on the list, or looked up if not — and opens it in place; a #project goes to its page; a task link to the task. Titles with punctuation work, brackets and parentheses included, because the match is on the whole title, not a parse of it. e2e: after linking a note, clicking its chip opens the note it names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4f25905d81 |
feat(calendar): a task's length is drawn, resizable, and editable
A timed task was drawn at a fixed half hour whatever its `duration_min` said, so a length the data model already carried was invisible and uneditable. Three parts: - The calendar draws a task at its real length — a quarter-hour floor so a zero-minute task is still grabbable, the day's end as the ceiling — instead of a flat thirty minutes. A ninety-minute task now looks like ninety minutes. - A grip along the foot of a timed task drags its length. Pointer events, not the HTML5 drag the body uses, and stopPropagation between them — otherwise making a task longer would pick it up and move it instead. It snaps to the quarter hour, follows the handle live, and writes once on release through a new store `resize`. Moving a task (the existing day/time drag) leaves the length alone. - The task drawer gains a length field beside the time — minutes, shown only with a time to hang it on, since a duration without a start is a length of nothing and the calendar has nowhere to draw it. So a length can be set off the calendar too. e2e: dragging the foot of a task makes it longer both on screen and in what the server stores, and the time does not move. Full Chromium suite green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
97e6be3afa |
fix(notes): gather sharing behind a Del button so the footer stays tidy
The sharing controls were laid out across the note's footer — the project select, the whole "Del med personer" panel and the Save/Delete buttons all on one row. It crammed: "Læg i projekt" wrapped to two lines and the people panel ran into the rest. Now one Del button in the footer opens a popover that holds both ways a note is shared — a project and named people — stacked in a small card. The footer is a status on the left and Del / Gem / Slet on the right, and nothing wraps. The button carries a count when the note is shared with someone, so "shared" is legible without opening it. The popover closes on a click outside, on Escape, and on switching to another note — the last tracked by note id, so a save returning a fresh copy of the same note does not slam it shut mid-use. The inner "add someone" button is now Tilføj, so it does not read as a second Del. e2e updated to open the popover before reaching the controls; full Chromium suite green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
91671a4f97 |
feat(notes): share a note with a person, not only through a project
Until now a note was shared exactly one way: file it in a shared project, and
everyone who could read the project could read the note. That is right when the
note belongs with a body of work and wrong when it is just "let Sofie see this" —
it made you stand up a project to hand over a single page.
A note can now be shared with named people directly, at viewer or editor — the
same two roles a project membership uses, so there is one idea of a role in the
program and not two. The paths stack: a note can be loose, filed, shared with
people, or any mix, and access is the most any of them allow.
The store:
- note_shares (migration 0030): note ↔ user ↔ role, one row per person, owner
recorded. Visibility (AllNotes, and search via mayTouchNote) widens to include
notes shared directly with the reader.
- the effective role folds a direct share on top of the owner and project paths.
The rules, drawn where they protect the owner:
- read = viewer, edit text = editor, on any path.
- MOVE a note (change its project) is owner-only: an editor filing it into a
project the owner is not in would take the owner's own note away from them.
- DELETE is owner or project-editor — a direct share, however high its role,
does not reach to throwing the note away.
- SHARE and UNSHARE are the owner's alone, like a project's invitations.
- the picker is everyone on the instance (a closed, invited set) minus who is
already on the note; a made-up id and the owner themselves are refused.
The list:
- a "Delt med mig" group at the top, collapsible, that appears only when it has
something in it — the same rule Favoritter follows — so it costs nothing when
you share nothing. A shared note lives there, not also in a time bucket.
- every shared note carries a small chip in its owner's avatar colour, the
project mark's shape said for a person.
- no star or archive button on a note somebody else owns: those change the note
itself, and it is not yours to put away.
- the editor grows a "Del med personer" panel for the owner: who can see it, at
what role, and an add row drawn from the instance's other people.
The old comment in the notes footer argued against a note having an access list
of its own. That was the right call until the case it did not cover — sharing
without a project — turned out to be the one actually asked for. Updated to match.
Person now carries json tags, so a note's owner serialises as id/name/avatar_color
like every other person in the API rather than as Go field names.
Tests: a shared note reaches the other person marked and owned; a viewer reads but
cannot write; an editor writes but cannot re-share or delete; unshare removes
access and is owner-only; the candidate list excludes who is already on and the
owner; and a made-up id and self are refused. OpenAPI covers the three new routes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
2219f6af13 |
fix(i18n): en gentagelse siges nu på læserens sprog
Release / Build and publish image (push) Failing after 10m2s
`recurrence.Describe` skriver dansk. Fladen kunne navngive de fem almindelige regler selv, og alt andet — "hver mandag", "hverdage", "hver anden uge tirsdag" — faldt tilbage til serverens tekst, som er dansk uanset hvad kontoen er sat til. Serveren kan ikke bare tage en locale med, og det er ikke dovenskab: `toTaskJSON` har niogtyve kaldesteder, og mange af dem er WebSocket-udsendelser, hvor én nyttelast når alle i et projekt på én gang. Der findes ikke ét sprog at skrive den i. Men reglen er den samme, uanset hvem der læser den. Den ligger allerede i nyttelasten, så beskrivelsen hører hjemme dér, hvor læseren er — i fladen. Serverens tekst bliver stående som reserve. Den er stadig den, et API-kald og en MCP-forbindelse får, og de har ingen flade at oversætte i. Ugedagen kommer fra `Intl` og ikke fra en tabel — ikke for at spare syv strenge pr. sprog, men for at få de navne, sproget faktisk bruger. Dagene sorteres, som ugen går, frem for som reglen tilfældigvis skrev dem: "mandag og torsdag" og "torsdag og mandag" er den samme regel, og den ene af dem er den, folk siger. Og to har sit eget ord på begge sprog — "hver anden", "every other". Det er ikke tallet to sat ind i den samme sætning, så det er sin egen nøgle. Et sprog, der bøjer enheden efter tallet, kan ikke sættes sammen af stumper; derfor står tallet inde i teksten frem for at blive limet på. Prøven bruger `hver mandag` og `hverdage`, fordi de netop *ikke* står på listen over de fem, fladen altid har kunnet. En tredje vending, jeg først skrev prøven med — "hver anden tirsdag" — viste sig ikke at være en, hurtig tilføjelse overhovedet læser: den lagde "hver anden" i titlen og gjorde "tirsdag" til en dato. Det er en anden sag, og den står nu skrevet ét sted, hvor den kan tages op. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
dcb71f4c14 |
feat(calendar): abonnement på flere kalendere, ICS og webcal
Release / Build and publish image (push) Failing after 10m3s
Bedt om som "abonnering af flere kalendere (ics og webcal?)". Det er også den eneste vej for en privat konto: instansens OAuth-klient er Internal, og Google afviser en konto udefra med `org_internal`, uanset hvad der ellers er sat op. **Samme tabeller.** Et abonnement er én kalender med begivenheder i, og det er præcis det, `calendars` og `calendar_events` allerede beskriver — med vinduet, farven, `shown` og hele fejemaskinen omkring sig. En tabel til ville være en anden vej til det samme sted, og to veje til ét sted er to steder at rette den dag noget om begivenheder ændrer sig. Det, der skulle laves om, var identiteten. En OAuth-konto er én pr. person og udbyder — tokenet er identiteten — mens et abonnement er én pr. adresse, og man har flere. `calendar_accounts_once` er derfor delvist nu: det holder for rækkerne uden adresse, og et andet indeks holder adresserne unikke. Delvise indekser er den ene ting, SQLite kan her, som ellers krævede en tabelombygning. **`webcal://` skrives om til `https://`.** Det er ikke en protokol, noget taler — det er en konvention, der får et skrivebord til at give adressen til kalenderprogrammet i stedet for til browseren, og alle udgivere skriver deres links sådan. At afvise den ville være at afvise folk på en teknikalitet. **Adressen gennem `safedial`, begge halvdele.** Afvist hvor den skrives ned, så den, der står med feltet, får svaret med det samme — og igen på den *opslåede* adresse hver gang den hentes, fordi et navn, der blev godkendt ved indtastningen, kan svare 127.0.0.1, når fejemaskinen kører klokken tre om natten. Det er den sætning, `internal/safedial` selv står og advarer om, og den blev skrevet efter et SSRF-fund i morges. **Hentet én gang, før den gemmes.** En adresse, der ikke kan læses, er en tastefejl langt oftere end en server, der tilfældigvis er nede, og at sige det, mens nogen står med feltet, er de to sekunder værd. Et abonnement, der lydløst aldrig fyldes, er den samme tavshed, notifikationerne havde. Læseren er ny i `internal/ics`, som hidtil kun kunne skrive. To ting, den med vilje ikke gør, står skrevet ved den: den udfolder ikke gentagelser, og den opløser ikke tidszoner. Begge dele er større end de ser ud, og begge har ét sted at høre til den dag de skal. En ting, formatet lærte: DTEND er eksklusiv for en heldagsbegivenhed. Gemmes den, som filen skriver den, dækker hver eneste helligdag også dagen efter sig. Én adresse, der fejler, stopper ikke de andre. Et abonnement er en anden persons server, og den ordinære tilstand for en anden persons server er, at den er i orden indtil en morgen hvor den ikke er. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
968a29ca67 |
feat(notes): listen står i grupper, og projektet har sin egen farve
Release / Build and publish image (push) Failing after 9m56s
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> |
||
|
|
fcd6dca852 |
fix(push): "der kommer ikke nogen" kunne ikke besvares
Release / Build and publish image (push) Failing after 10m0s
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> |
||
|
|
ae673086bc |
feat(tasks): tid på en opgave, og et træk i kalenderen sætter den
Release / Build and publish image (push) Failing after 9m58s
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>
|
||
|
|
b87c9ea6e2 |
fix(auth): en afvist passkey sagde "forkert e-mail eller adgangskode"
Release / Build and publish image (push) Failing after 10m0s
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> |
||
|
|
aadfb197b6 |
feat(notes): arkiv, flere ad gangen, og en kopiér-knap på koden
Release / Build and publish image (push) Failing after 9m55s
**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> |
||
|
|
8aad7c13b0 |
perf(notes): listen sendte hele noter for at vise halvfems tegn af dem
Release / Build and publish image (push) Failing after 9m55s
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> |
||
|
|
ce3fc466b2 |
fix(auth): andenfaktoren kunne gættes igennem, og et token kunne tilmelde sin egen
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> |
||
|
|
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> |
||
|
|
dcd66b415e |
docs(web): registreringsteksten fortæller nu, at klienten dækker begge dele
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. |
||
|
|
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> |
||
|
|
031bb18067 |
fix(calendar): en tom kalender skal sige hvorfor, ikke bare være tom
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. |
||
|
|
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> |
||
|
|
987d4edfce |
feat(calendar): en læst Google-kalender, med sit eget endpoint og sin egen sweep
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. |
||
|
|
78891bae6f |
feat(beacon): tæl installationer, sig præcis hvad der sendes, og lad folk sige nej
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
414a35eaba |
feat(notes): billeder og bilag følger med ind — og ud
Release / Build and publish image (push) Failing after 9m58s
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> |
||
|
|
11fdbefe1b |
feat(notes): hver note som en Markdown-fil, i et zip
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> |
||
|
|
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> |
||
|
|
90bdf4b725 |
feat(look): en akse mere — hvordan det læses, ikke hvor lyst det er
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> |
||
|
|
1bbae698d5 |
feat(notes): en note deles ved at ligge i et projekt
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> |
||
|
|
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> |
||
|
|
c9dfffd95a |
feat(mailboxes): postkasser kan forbindes fra fladen
Release / Build and publish image (push) Failing after 9m59s
Panelet under Integrationer: tilføj, hent nu, kobl fra. Sit eget frem for en tilstand mere inde i Gmail-panelet, for de to er ikke det samme i forskellige klæder — det ene er en app, man logger ind igennem, det andet et kodeord, man selv holder. Feltet hedder IMAP-server og ikke Server: der stod i forvejen Server på CalDAV længere nede, og to felter med samme navn på én side er tvetydige også for en skærmlæser. Testen fandt det, men det var ikke kun testens problem. To vagter fangede mangler undervejs, og begge havde ret: mailbox_failed skulle have en tekst i fladen, ellers ville nogen få serverens engelske logsprog at se, og de tre ruter skulle i openapi.yaml. Der er nu et Mailbox-skema samme sted, som siger hvad der ikke kommer tilbage — kodeordet og eventuelle tokens. E2E-prøven forbinder til noget, der ikke lytter, og ser efter værtens egen afvisning på skærmen. Den er samtidig prøven på at 422 var det rigtige valg: var det stadig 502, ville et mellemled kunne kaste beskeden væk, og der ville stå "noget gik galt". 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> |
||
|
|
a38a79e9b6 |
feat(secrets): tokens og kodeord forlader ikke længere huset med en kopi
Gmails refresh-token lå i klartekst i databasen. Det er ikke filen på serveren, der er problemet, men kopierne af den: sikkerhedskopier kan hentes ned gennem fladen, så hver eneste kopi, der er taget indtil nu, bar et levende token. Derfor ligger nøglen uden for databasen — i miljøet, eller i en fil ved siden af dataene med tilstand 0600. VAPID-parret er præcedens for en hemmelighed, instansen selv laver og gemmer i basen, men det mønster dur ikke her: en nøgle i samme fil som det, den låser, er ingen lås. Backup er VACUUM INTO af selve databasen, så filen følger ikke med. Kun de navngivne felter forsegles — refresh_token, access_token, password, client_secret. Resten af klatten er det, man læser, når en postkasse driller: hvilken mappe, hvilken etiket, hvilken adresse. At kryptere det med ville ikke købe noget og ville koste muligheden for at kigge. Ingen migration, og ingen der skal forbinde forfra: en værdi uden mærket læses som sig selv, og næste skrivning lægger den forseglet på plads. Kolonnen konverterer sig selv efterhånden som den bruges, i stedet for i én omgang, der skal have ret om hver eneste række på én gang. Der er en prøve for netop det. Fladen siger det på siden, hvor man henter kopien — for den dag nøglen betyder noget, er den dag, man gendanner et andet sted og undrer sig over, at postkassen er tavs. Runen er på v4; en ny variabel uden et versionsspring er usynlig. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
dab1827c14 |
fix(i18n): en engelsk flade sagde "2 opgave(r) hentet"
Release / Build and publish image (push) Failing after 10m6s
Beskeden efter "Fetch now" stod på dansk uanset sprog, og med et "(r)" i stedet for rigtig bøjning. Den bruger nu t() og plural() som resten. Vagten burde have fanget den og gjorde det ikke: den krævede et anførselstegn lige efter app.toast(, og denne stod som en skabelonstreng inde i en ternær hen over to linjer. Den læser nu hele argumentlisten frem til den matchende slutparentes, så det er formen på kaldet og ikke tilfældig placering, der afgør, om en streng bliver set. Sat på prøve ved at lægge den gamle tekst tilbage: begge strenge blev fanget. 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> |
||
|
|
2b45aa33e1 |
feat(admin): ask the panel to restart, from verdande's own settings
Release / Build and publish image (push) Failing after 10m2s
A container cannot replace its own image from the inside, so "update" is really "ask whoever owns the container to recreate it" — Yggdrasil pulls `:latest` on the way, which is what makes a restart an update at all. This removes the second browser tab, not the panel's authority: the panel still does the work and still records who asked. Three settings, needed together, and the page names whichever are missing — an operator reading "not configured" should not have to go and find out which of three. The token is never sent to a browser; the page is told only whether it is set. Answers 202 rather than waiting. The panel stops this container as part of answering, so the reply may never arrive, and a request cut off mid-flight is the successful case wearing a failure's clothes. The interface believes neither answer and polls /healthz until it responds. Administrators only, and sessions only. A leaked API token that could restart the server is a denial of service in one request. 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> |
||
|
|
9cc7df69f0 |
fix(projects): the header stops crushing the project's name
"GarageRisteriet" rendered as "GarageRist / eriet" — broken mid-word. A name split
mid-syllable does not read as a wrap, it reads as a rendering fault.
Two causes. The row carried eight controls beside the heading — colour, show-done,
three view buttons, share, history, delete — and the heading was the only flexible
thing in it, so it was what gave way. And `overflow-wrap: anywhere` breaks at the
first opportunity rather than the last necessary one, so what gave way, gave way
inside a word.
Everything but the view switcher is behind one button now. What stays out is what
you use while working — which view you are in. What moves in is what you do *to* a
project rather than in it, and most of it once ever. The heading gets the row back,
and `break-word` only splits a word that could not fit on a line of its own.
The menu closes on Escape and on a click anywhere else, listened for in the capture
phase so a click whose own handler stops propagation still closes it. A menu you
can only leave by pressing its own button again is one you click twice to get out
of.
Three more untranslated strings turned up on the way: "Fjern {member.name}", "Luk
menu" and "En slettet konto". The guard missed them because none of its words
covered them — it knows fjern, opret, vælg, luk, tilføj, ryd, gem, slet and omdøb
now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
ab5e3e35a6 |
fix(i18n): three Danish strings that had no æ, ø or å in them
Spotted on the running instance: the version line read "Running v0.11.0. Der bliver ikke set efter opdateringer." — half translated, on one line. The sweep that was supposed to catch these grepped for Danish letters, and none of the three survivors contain one: "Ingen nyere version fundet.", "Ikke mere i dag." and a token hint split across two source lines so neither half looked like prose. So there is a test for it now. It reads the templates and looks for the words that give Danish away whatever letters they are spelled with — a word list rather than a language detector, because this ships as one static binary and thirty common words catch the sentences a person actually writes. It fails on the exact three that got through. 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> |
||
|
|
626ea9aaec |
feat(tasks): finished work can be looked at, and is out of the way by default
A task list is what is left to do, so closing something has to take it off the plan. But "what did I get done" is a real question and the answer was nowhere: once a task was closed it left the interface entirely. They come back in one list at the bottom rather than back among the open ones in their sections. A closed task has stopped being work and started being a record, and putting the record in the middle of the plan makes the plan longer without making it say more. Off by default, and deliberately: a list that opens full of things already done is a list you have to read past before you can use it. In localStorage rather than on the account, like the sidebar's width and unlike a folded group. It is a way of *looking*, not a statement about the work — somebody reviewing their week wants it on for ten minutes, and a setting that followed them to every device is one they would have to turn off twice. `loadTasks` asks for closed tasks when the setting is on, rather than each caller deciding. A view added later cannot then forget and quietly ignore it. 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>
|