alarm-i-saetningen
234
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6116ed2e21 |
fix(notes): [[ completes a note link, the way # completes a project
Typing [[ did nothing. The toolbar hint offered "[[ another note" and the parser turned a fully-typed [[Title]] into a link on save, but the completion that makes it usable — the one # has for projects — was never there, so you had to know and type a note's title exactly, brackets and all. Now [[ opens the same picker # does, over the notes the page has loaded: the current note is left out (a note linking to itself is a loop nobody meant), and one without a title yet cannot be pointed at. Choosing a note closes its own brackets — the opener is already in the text — and leaves a space, so the next word is a word and not more of the tag. On save the link is real and shows in the note's links, not merely as text. The picker's two kinds are told apart where a project wears its colour: a note suggestion wears the brackets that open it. e2e: [[ offers a note by title, choosing it writes [[Title]] into the note, and the link lands in the note's links. Full Chromium suite green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
fcedab0b19 |
docs(site): add the new features and light screenshots of them
The landing page did not mention two things the app grew — sharing a note with a person, and task durations — and its screenshots were all in the dark theme. - Copy: a cell for sharing a note or a page (to a person, or by filing it in a project, with the Shared with me group), and one for how long a task takes (drawn to length on the calendar, dragged by its foot, kept when moved). - Two new screenshots in the light theme, at the top of the gallery: the calendar drawing timed tasks to their real length, and a note somebody shared gathered under Shared with me. English throughout, like the rest of the site — the capture account is English, so the interface is. The generator grew the demo data these need: a handful of timed tasks with lengths across the week, and a note the colleague shares back to the owner. Every other screenshot was recaptured from the current build in the same pass, so the whole set reflects what ships today (the tidied note footer, and the rest). 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> |
||
|
|
f0d18bb800 |
fix(notes): let a monospace line be left — on return and through the menu
A <pre> is preformatted, so the browser answered Enter by inserting a <br> and
keeping the caret inside it. A monospace last line therefore trapped everything
typed after it in monospace, with no way back to body by typing — and the style
menu could not get out either: `formatBlock('p')` on a line inside a <pre> nests a
<p> *inside* the block (`<pre>…<p>body</p></pre>`), so the text stayed monospace
and the note was left malformed. Reported as "monospace keeps going and I can
neither delete it nor switch to body".
Two fixes, both in the editor:
- Plain Enter in a <pre> now steps out: the tail after the caret becomes a
paragraph below the block, and that is where the caret lands. Shift+Enter is
untouched, so a soft line break still writes multi-line code inside the block —
Enter to leave, Shift+Enter to stay.
- The style menu converts anything touching a <pre> by hand, replacing the element
rather than reformatting inside it, so a monospace line goes cleanly back to body
(or to a heading) instead of nesting.
The split can leave an empty text node in the new paragraph, and a block with no
element and no text is not a caret host in Chromium — the next keystroke falls back
into the block above. So an empty paragraph is given a <br> to stand in, which was
the difference between the caret landing in the new line and the text reappearing
in the code.
e2e: a monospace line left on Enter is body after it, Shift+Enter stays monospace,
the menu returns it to body, and the whole thing survives the round-trip through
Markdown and a reload.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
cdab817141 | Merge main: pick up the calendar e2e flake fix | ||
|
|
711fb4838b |
test(e2e): the calendar test counted other tests' tasks as its own
The calendar test asserted `[data-date=day] .tevent` had a count of exactly one — the mocked meeting — after dragging its task to the next day. That count assumed the day held nothing but this test's own events. It does not: the suite shares one database, and an earlier test drags a task across a month boundary, which on some dates lands a timed task on this very day. A timed task renders as a `.tevent` too, so the count was two, and the test failed — but only in the full run, and only when the date arithmetic put the two tests on the same cell. Alone it passed, which is exactly the shape of a shared-state collision rather than a real bug. Named, not counted: assert the meeting is still there and the task is no longer in the day's column, which is what "the events stayed, the task moved" actually means — and is true however full another test left the day. Same for the all-day band. No product code changed; the drag and everything it checks are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
643b30555e |
test(e2e): follow the renamed share label, and cover person-to-person sharing
The project-filing control was relabelled "Del i" → "Læg i projekt" and its empty option "Kun mig" → "Intet projekt" (a note can now be shared with people while under no project, so "kun mig" was no longer true). The two e2e tests that drove that control by its old label are updated to the new one. And a new test for the feature itself: an owner shares a note with a second account from the Del med personer panel, the recipient's own list grows a Delt med mig group with the note in it, a viewer's row carries no star or archive control, and unsharing takes it back off their list. The recipient is navigated on the origin the invite signed her in on — VERDANDE_BASE_URL is localhost while the suite drives 127.0.0.1, and a session cookie set on one host is not sent to the other. 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>
|
||
|
|
acd6784333 |
test(e2e): kalendertesten afhang af, hvilken ugedag den blev kørt på
Grøn i månedsvis, rød i morges — på en commit, der ikke var i nærheden af en kalender. Det kostede en runde at tro, det var min egen ændring. Testen lægger opgaven på i morgen og trækker den til dagen efter. Er i morgen en søndag, er kilden sidste kolonne i sin række og målet første celle i den næste — nede i den del af gitteret, et 720 px vindue skærer af. Et syntetisk træk slipper i målets midtpunkt uden den auto-scroll, en rigtig markør får af browseren, så et mål under folden bliver aldrig ramt. I går lå begge celler i samme række. To ting rettet, og den anden er den vigtigste: - Vinduet er højt nok til alle seks rækker under månedsdelen. - Målet tages fra gitteret frem for at blive regnet ud af datoen. Dagen efter i morgen ligger ikke altid i den måned, der vises — i enden af en kort måned falder den udenfor — mens cellen ved siden af altid gør. Med et fald tilbage til cellen før, for den ene dag om måneden hvor der ikke er en efter. Appen fejler ikke noget: en rigtig bruger ruller. |
||
|
|
b6cbc6cbc2 |
fix(ai): din egen model kunne ikke sættes op — tre fejl i én oplevelse
Rapporteret som "AI gemmer ikke mine oplysninger". Anden gang det symptom meldes, og en helt anden årsag end sidst — dengang delte scopes i user_settings én række og slettede hinanden. Udbyderen hedder "OpenAI-kompatibel (Ollama, LM Studio, …)" og feltet foreslår selv http://localhost:11434/v1. safedial nægter præcis den slags adresser. Så den ene udbyder, der findes for at køre sin egen model, kunne ikke sættes op, og formularen foreslog en adresse, serveren ville afvise. Serveren svarede 422 med en fejl på base_url. Formularen havde ingen plads til at vise en fejl på det felt, og fordi fejlobjektet ikke var tomt, blev der heller ikke vist en toast. Beskeden forsvandt, og det så ud, som om der ikke skete noget ved at trykke Gem. Tre rettelser: - Private adresser tillades for admins. Muren står, fordi en server, der henter en brugerangivet adresse, lader den, der angiver den, nå ting han ellers ikke kan. En admin på en selvhostet instans ejer allerede værten og dens netværk, så det giver ham intet nyt. For en inviteret bruger er den lukket som før. Tilladelsen regnes ud pr. forespørgsel, ikke gemt ved siden af adressen — den, der ikke længere er admin, skal ikke kunne nå indenfor på et gammelt flag. - Tailnet er ikke "indenfor". Halvdelen virkede allerede ved et tilfælde: Gos IsPrivate dækker RFC1918 og ikke det CGNAT-område, Tailscale låner, så 100.x slap igennem uden at nogen havde besluttet det. IPv6-halvdelen gjorde ikke — fd7a:115c:a1e0::/48 ligger inde i fc00::/7. Samme maskine var altså tilgængelig eller ej alt efter adressefamilie. Begge områder er nu navngivet, hvilket gør IPv4-halvdelen bevidst frem for heldig. - Feltfejl kan ikke længere forsvinde. base_url og model har fået et sted at vise en, og en fejl på et felt, der ikke har et, råbes op som en toast frem for at blive tabt. |
||
|
|
8d5dd1f1c3 |
fix(notes): grupperne stod tomme — hver liste rullede for sig
Release / Build and publish image (push) Failing after 9m59s
Meldt ind som "noter vises ikke i den nye listeform": hver eneste overskrift stod der med sit tal, og der var ikke en eneste note under nogen af dem. Før var der ét `<ul>`, og *det* rullede. Da listen blev delt i grupper, blev hver gruppe sit eget `<ul>` — og dermed sit eget flex-element med `overflow-y: auto`. Flex-elementer skrumper som udgangspunkt, og et element, der skrumper og klipper, viser ingenting. Med tyve måneder i søjlen fik hver liste nul at være høj i. Rulningen hører til kassen om dem alle, ikke til hver af dem. **Prøven fangede det ikke, og det er værd at forstå hvorfor.** Den lavede to noter, altså tre grupper, i et vindue på 720 pixels — og med så få grupper er der plads nok til, at skrumpningen ikke bider. Fejlen kræver pres. Prøven gør vinduet lavt nu og kigger efter, at rækkerne kan *ses* og ikke bare findes, og den siger desuden hvor rulningen ligger: på kassen, ikke på listerne. Den er set fejle med den gamle stil. 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> |
||
|
|
868588de10 |
feat(projects): et projekts kalender kan vises som en uge
Ugen fandtes på Kommende og på Kalender, men ikke dér hvor et projekt bor — og det
er dér, "hvornår sker de her fire ting" oftest bliver spurgt.
Inde i kalendervisningen frem for som en fjerde `view_mode`. `view_mode` er
projektets og delt med alle, der kan se det, mens uge eller måned er et spørgsmål
om hvor bred skærmen er, foran hvilken der sidder én person — samme grund som
sidebjælkens bredde ligger i localStorage af. Og en fjerde værdi ville kræve en
tabelombygning: `CHECK (view_mode IN ('list','board','calendar'))` kan SQLite ikke
ændre på plads.
**Og så fandt den en fejl, der har ligget og ventet.** `cursor` blev sat med
`span === 'week' ? … : …`, hvilket læser `span` i det øjeblik komponenten bliver
til og fanger den værdi for altid. Byggeriet har advaret om det lige så længe. Så
længe hver visning havde sin egen komponent, gjorde det ingen forskel — og det
gjorde det i samme sekund, en vælger begyndte at skifte `span` på den samme.
Markøren stod på den 1. i måneden, og et skift til uge landede på ugen omkring den
1.: "27. jul.–2. aug." for en person, der kiggede på august.
Rettet, med den ene undtagelse skrevet ned: en månedsvisnings markør *er* den
første i måneden, så et skift til uge, mens man står i den indeværende måned,
skal lande på den uge, i dag ligger i — ikke på ugen omkring den 1.
Og en anden fælde, jeg selv gik i undervejs: en underklasses felter sættes *efter*
basens konstruktør er kørt. Nøglen til localStorage som et felt på en underklasse
betød, at valget blev gemt ét sted og læst et andet — en uge, man havde valgt, var
en måned igen efter en genindlæsning. Nøglen er et argument nu; et argument har
ingen rækkefølge at tage fejl af.
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>
|
||
|
|
9d34b57166 |
feat(calendar): ugen er et døgn, og dagene er lige brede
Release / Build and publish image (push) Failing after 9m58s
To ting, meldt ind fra en uge, der ikke svarede på det, man åbnede den for. **Dagene var ikke lige brede.** Gitteret stod på `repeat(7, 1fr)`, og `1fr` er `minmax(auto, 1fr)`: `auto` som minimum betyder, at en søjle ikke kan blive smallere end sit længste ord. Én opgave, der hed "Gårdbutikken på Møllerup Gods — opfølgning på mail", gjorde onsdag dobbelt så bred som mandag og klemte resten af ugen sammen om den. `minmax(0, 1fr)` lader søjlen blive så smal, den skal, og så klipper brikken sin tekst af — hvilket den allerede kunne. **Ugen var syv punktopstillinger.** Den svarede på "hvad skal der ske torsdag" og ikke på "hvornår", og det andet er det, man åbner en uge for. To møder klokken ti er en konflikt, man skal kunne se, ikke læse sig til. Nu et gitter med en tidsrende og syv dage: timerne ned ad siden, begivenheder placeret efter klokkeslæt, og et bånd foroven til det, der ikke har et — en heldagsbegivenhed lagt klokken nul ville påstå, den holdt fra midnat til midnat. Overlappende ting deler bredden i sammenhængende klynger frem for parvist: overlapper A med B og B med C, skal alle tre dele, også når A og C ikke rører hinanden. Ellers ville A og C hver tro, de havde halvdelen, og lægge sig oven på hver sin halvdel af B. Hvilke timer der tegnes, kommer fra ugens egne begivenheder med arbejdsdagen som gulv. Et helt døgn er fireogtyve rækker, hvoraf de otte altid er tomme, og så er gitteret noget man ruller i frem for noget man ser — men en vagt klokken fem skal med, uden at alle andre uger betaler for den med tomme rækker. Måneden er ikke rørt ud over søjlebredden. En dag i en månedsrude er halvanden centimeter høj; et døgn tegnet i den er ikke en visning, det er en streg. Brikkerne er flyttet ud i to snippets, som begge visninger tegner med. De viser de samme to ting og skal vise dem ens; det er placeringen, der er forskellig, og så er det kun placeringen, der står to steder. `data-date` følger med over på ugens timesøjler — det er sådan noget uden for komponenten peger på en dag. Båndet får sit eget `data-allday`, så de to ikke bliver til det samme svar på ét spørgsmål. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
ce1d27f8ca |
fix(notes): én note, der ikke kunne tegnes, låste hele ruden
Release / Build and publish image (push) Failing after 9m53s
Meldt ind som "jeg kan ikke skifte mellem noter — uanset hvilken jeg trykker på, åbner de ikke". Editoren indlæste med `note.body.trim()` uden værn. `body` er en streng fra serveren hver eneste gang — indtil den ikke er, og så kaster den her inde i et `$effect`. En effekt, der kaster, tager komponentens opdateringer med sig: den forrige note bliver stående, og *ingen* note kan åbnes bagefter. Klikket kan ikke engang lande, hvilket er derfor det ikke ligner en fejl i noten, man klikkede på, men i hele ruden. To ting, ikke én. Værnet, så en manglende krop er en tom note. Og en ramme om tegningen, så det næste, der en dag ikke kan tegnes, heller ikke bliver til det her: en note, der fejler, viser sin egen tekst uden formatering, og resten af ruden virker videre. `loadedId` sættes nu før tegningen frem for efter. Ellers ville effekten prøve igen ved hver eneste ændring bagefter og kaste hver gang. `textContent` og ikke `innerHTML` i faldbakken: teksten er ikke tegnet, og at lægge den ind som markup ville være at køre præcis det, tegningen nægtede. Gemningen er ikke i fare — den skriver `draft`, som kommer fra svaret og ikke fra det, der står i editoren, indtil nogen taster i den. Prøven tvinger formen frem i stedet for at vente på den nyttelast, der gjorde det. Hvad der end fik `body` til at mangle, er svaret det samme: én note må ikke kunne låse de tolv hundrede andre ude. Den er set fejle uden rettelsen — med netop den fejl, konsollen viste. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8ecefbbdc2 |
fix(notes): rullebjælken svævede midt i arket
Det er `.page`, der ruller — og det var også `.page`, der blev smallere for at holde linjelængden nede. Så bjælken fulgte med ind til tekstsøjlens kant og stod med en håndflade tom rude til højre for sig, en tredjedel inde i ruden. Grænsen sættes som polstring i stedet for som bredde. Elementet er lige så bredt som ruden — bjælken bliver ved kanten, hvor den hører til — og teksten er nøjagtig lige så bred som før. `max()` mod nul, fordi en smal rude ellers ville få negativ polstring, og `border-box` er sat globalt, så bredden er hele boksen og ikke boksen plus polstringen; uden det ville det give en vandret rullebjælke. Kildevisningen deler `.page` med editoren og havde samme fejl. Den følger med. Målt: elementet slutter nu på rudens indholdskant i stedet for 74 px inde, og `scrollWidth - clientWidth` er nul. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e46aaa462a |
feat(projects): sektioner kan trækkes op og ned, og båndet står ikke længere skævt
Rækkefølgen af sektioner *er* planen — "Planlægning" står før "I gang", fordi det er den vej arbejdet går — og indtil nu kunne den kun laves om ved at slette en sektion og lave den igen, hvilket smider dens opgaver ud i det usektionerede område på vejen. Hele overskriften er håndtaget. En sektionsoverskrift indeholder ikke noget, der selv bliver trukket, så der er ingen geste at stjæle — hvilket er grunden til, at sidebjælkens rækker skulle have et greb, og grunden til at disse ikke skal. Ikke draggable mens den omdøbes: et draggable element lader ikke markøren vælge tekst i det felt, det indeholder. Serversiden er hele listen på én gang som projekter og grupper, og af samme grund: et projekt har en håndfuld sektioner, så 0, 1, 2 … er én lille transaktion uden præcision at løbe tør for, og en liste kan ikke ende halvt anvendt. `ReorderSections` er scopet til projektet og ikke til en ejer — en sektion har ingen ejer, den hører til et projekt, og om du må rykke rundt er et spørgsmål om det projekt, som handleren allerede har stillet. Id'er fra et andet projekt springes over af WHERE-klausulen; der er en prøve på præcis det, fordi ingenting ellers kontrollerer id'erne enkeltvis. Båndet stod fire pixels skævt. Det var trukket ud med --s2 og polstret med --s3 — samme idé gjort med to forskellige tal — så det blev otte pixels bredere end listen i hver side, og teksten landede fire pixels uden for den kolonne, rækkerne holder. Til venstre var det usynligt. Til højre var det Omdøb og Slet, der stod fire pixels forbi alt over og under sig, hvilket er præcis langt nok til at ligne en fejl og ikke langt nok til at se hvorfor. En række er `.row` med `padding: var(--s2)` inde i en boks uden, så båndet har nu samme polstring og ingen margen. Board-visningen er ikke rørt: der er kolonnerne vandrette, og det er en anden geste. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
98bfa216d6 |
fix(look): mærket og de faste visninger fulgte med op, når listen rullede
Hele sidebjælken rullede, så runen og I dag forsvandt over kanten, så snart der var projekter nok — og de er dem, man er på vej hen til oftest. Alt under de faste visninger ligger i en rulleboks nu, så rullebjælken begynder under dem. En boks indeni frem for `position: sticky` udenom: et klæbende hoved efterlader rullebjælken i fuld højde ved siden af sig, og det var netop den, der skulle starte længere nede. Snippet-erklæringerne bliver liggende udenfor — de tegner ikke noget selv, men de kaldes oppe fra .views, og en snippet erklæret indeni hører kun til indeni. Chevronen for en mappe dækkede gruppens prik med et felt i --surface. På den sænkede bjælke er det en lys firkant, og en foldet gruppe har sin chevron fremme permanent, så klodsen stod der hele tiden. Prikken tones ud i stedet. Det kostede en fælde: en `opacity` under 1 laver en stakkontekst og maler elementet, som var det placeret med `z-index: 0`. Prikken lagde sig dermed oven på knappen, den lige var forsvundet for, og tog klikket — usynlig og stadig i vejen, så gruppen ikke kunne foldes. Ét lag op på knappen, og rækkefølgen er som før. Prøven for vandret rul måler nu begge bokse. Bjælken selv er `overflow: hidden`, så en for bred række ville blive klippet dér og aldrig nå at tælle med — den ville bestå af den forkerte grund. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6c3d66108b |
fix(look): stregen under hurtig tilføjelse havde ingen luft under sig
Release / Build and publish image (push) Failing after 9m56s
Den skiller "skriv en ny" fra "det, der allerede er". Uden afstand sad den første sektion klods op ad den, så den læste som en kant om sektionen frem for som en afslutning på feltet over. En streg skal have plads på begge sider for at kunne ses som en streg. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1437667483 |
fix(notes): stjerne og arkiv står oven på hinanden
Release / Build and publish image (push) Failing after 9m56s
To knapper ved siden af hinanden er toogfyrre pixels taget fra en liste, der er tre hundrede bred — og de er det, man rører en gang imellem, mens titlen og uddraget er det, man læser hele tiden. I en søjle koster de toogtyve. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
035d079a8e |
fix(notes): teksten fulgte ikke med, når ruden blev bred
Release / Build and publish image (push) Failing after 9m59s
46rem var sat for at holde linjelængden i skak, og det er stadig det rigtige hensyn — men det blev sat som ét tal, og på en skærm halvanden gang så bred stod teksten i en smal søjle med en håndflade tom plads ved siden af. Kodeblokke og billeder, som er de to ting i en note, der *vil* have bredde, blev klemt sammen uden grund. clamp frem for et tal: en smal rude får det hele, en bred får en linjelængde, øjet kan finde tilbage i. På 1920 er tekstsøjlen nu 1088 px af arkets 1304 i stedet for 736. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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. |
||
|
|
eeaaf7506b |
docs: skærmbillederne er engelske, og der er to mennesker i dem
Landingssiden og dokumentationen læses på engelsk, og billederne var danske hele vejen — både grænsefladen og indholdet. Kontoen bliver nu oprettet med engelsk sprog frem for at oversætte noget her: det er sådan appen selv gør det, og det er også dét, der får en glemt oversættelse til at stå i et billede i stedet for at vente på en bruger. Demodataene er skrevet om til engelsk arbejde og husholdning. Og der er en person mere: en invitation, en konto, tre opgaver hun sidder på og en kommentar. Halvdelen af det, appen er til for, findes først, når der er to i den — en ansvarlig på en opgave, et navn i historikken, og "Venter på andre" som andet end en tom side. Tre billeder viste ikke det, teksten sagde. `detail` var en tom formular, så opgaven har fået beskrivelse, undertasks, en fil, en påmindelse og en samtale. `themes` viste profilfeltet over temaerne, så den ruller nu ned til dem. `delegated` var en tom tilstand. Noterne er kommet til — listen med en note åben, og den samme note som kilde. Fjorten billeder i alt. WebP'en skrives af den browser, der i forvejen står åben, frem for af cwebp: et program mere at installere hører ikke til i et script, hvis hele pointe er, at der ikke er noget at installere. PNG'erne ligger nu kun i docs/ og WebP'en kun i site/, hvilket er præcis dét, de to steder læser — før lagde et kørsel tolv usporede PNG'er i site/, som ingen brugte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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. |
||
|
|
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> |
||
|
|
052f8a43fb |
fix(notes): en note kunne smugle et script ind gennem et billedes alt-tekst
Indført i aften, sammen med billedvisningen, og fundet ved at læse den igen.
`escapeHtml` tager &, < og > — ikke anførselstegn, og det er rigtigt for tekst
mellem to tags, hvor et anførselstegn ikke betyder noget. `alt` er ikke tekst
mellem to tags. Det står inde i en attribut, og der lukker det første
anførselstegn den, hvorefter resten står som markup:

blev til et <img> med en onerror på. Adressen var i forvejen bundet til vores
eget bilagsmønster, så den var ikke vejen ind — teksten ved siden af var.
Det er ikke en teoretisk vej. En note deles ved at ligge i et projekt, så den
læses af alle, der kan læse projektet, og scriptet ville køre på vores eget
domæne med sessionscookien, hos den der åbnede noten.
En værdi i en attribut har brug for sin egen undslipning, og har nu en. Prøven
skriver noten, åbner den og ser efter, at hændelsen står som tekst i alt og ikke
som en attribut på elementet — og at billedets 404, som er selve vejen en
onerror ville blive kaldt ad, ikke kalder noget.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
dee2d5e30b |
fix(faerdige): en opgave lukket efter midnat lå under "I går"
Release / Build and publish image (push) Failing after 9m56s
Fundet af prøverne, som for første gang kørte forbi midnat. Dagen blev læst som `completed_at.slice(0, 10)` — de ti første tegn af et tidsstempel, der er i UTC. Overskriften sammenlignede med *lokal* dato. I dansk sommertid er de to ikke den samme dag i de første to timer efter midnat, så en opgave lukket 00.30 blev sat under "I går", indtil klokken blev to. Tyve timer i døgnet er de ens, hvilket er derfor prøven har været grøn hver eneste gang før. Dagen udregnes nu lokalt, som den overskrift den skal svare til. Og prøven for invitationslinket kunne kun bestå seks dage om ugen. Den ledte efter "males inden fredag" som én tekst, men hurtig tilføjelse læser "fredag" som forfaldsdatoen og tager ordet ud af titlen — tilbage står "males inden" plus et mærke, hvis ord skifter med ugedagen. Kørt på en torsdag hedder mærket "I morgen", og så fandt den ingenting. Prøven handler om invitationer, ikke om datoer, så den ser nu på titlen alene. Uddraget i notelisten viste `: 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> |