9155e799b14b94a7aecb773e499b73cdda177a2a
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3e52eea2da |
feat(kvasir): én mail bliver til én opgave, uanset hvem der læser den
Fundamentet under Kvasir, og det er værd at have uden ham. Intet kunne svare på "er denne mail allerede blevet til en opgave?". To læsere af samme postkasse — en anden assistent, en gentaget kørsel, et menneske — laver derfor to opgaver. Det er ikke teoretisk: en briefing lavede "Betal faktura fra Browns" to gange i samme kørsel. Opgaver har nu en `source_key`, navngivet med et rum foran: `gmail:thread:…`, `imap:<vært>:<mappe>:<uid>`. Et Gmail-tråd-id og et IMAP-uid ligner ikke hinanden, lige indtil den dag de kolliderer, og et kolon koster ingenting. Det betyder også, at den næste kilde ikke skal bruge en ny kolonne — pointen med en nøgle frem for en fremmednøgle. Der findes ingen messages-tabel at pege på, og det her skal virke uden. Et delvist unikt indeks er selve låsen, ikke en optimering: to agenter, der læser samme indbakke, SKAL være ude af stand til at lave to opgaver, og "skal være ude af stand til" er en betingelse frem for en aftale, nogen husker. Afgrænset til den, mailen kom til — to personer på samme instans, der begge modtager tråden, får hver sin opgave, og det er rigtigt. CreateTask svarer ErrDuplicate frem for at oprette nummer to, og begge mail-veje springer over i stedet for at fejle. IMAP-vejen tæller den stadig som læst — ellers rykker last_uid sig aldrig forbi den, og hver eneste kørsel fra da af standser samme sted. `TaskIDBySource` er spørgsmålet den anden vej, så en assistent, der ikke ejer triagen, kan se, at mailen er håndteret. Slettede opgaver tæller med: en opgave, der blev lavet af en mail og smidt væk, er også et svar. |
||
|
|
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. |
||
|
|
3e8d609316 |
docs: imaget hentes fra GHCR, og prøven for 'offentlig' var forkert
Adressen rettet fem steder tilbage til ghcr.io. Og rune-siden lærte folk at teste synlighed med `docker --config /tmp/empty pull`, hvilket er den fejl, der tog instansen ned: en tom config stopper ikke maskinens credential-helper, så pullet autentificeres uden at se sådan ud og melder succes for et image, ingen fremmed kan hente. Prøven er nu registrets token-endepunkt med curl. |
||
|
|
fcd2443007 |
fix(rune): tilbage til GHCR — Gitea kan ikke servere imaget uden login
v5 flyttede imaget til gitea.nolimit.dk og tog instansen ned. Panelets PullImage sætter aldrig RegistryAuth, så registret skal kunne læses uden legitimation, og det kan Gitea her ikke: token-endepunktet svarer 401 for kw/verdande, også efter at repoet blev gjort offentligt. Instansen kræver login på alt, registret inklusive. Jeg påstod det modsatte, og beviste det med en test, der ikke kunne bevise noget: `docker --config <tom-mappe> pull`. En tom config stopper ikke maskinens credential-helper, så pullet blev autentificeret uden at se sådan ud. Den rigtige prøve er registrets token-endepunkt med curl — GHCR svarer 200 med et token, Gitea svarer 401. Begge dele målt. Fejlen er værd at kunne genkende: panelet river containeren ned for at anvende runen, pullet fejler, pullImageRetry kasserer fejlen, og ContainerCreate slår derefter op lokalt og siger `No such image`. Serveren står som "running" uden container_id, udadtil en 502, og intet sted nævnes det manglende login. Imaget ligger nu begge steder — samme digest, sha256:f7b23efd… — så koden bliver i Gitea, mens containeren hentes fra GHCR, indtil Gitea kan servere anonymt. |
||
|
|
27a7b04ff3 |
test: kassens rigtige tailnet-adresse ud af testen
Jeg skrev instansens faktiske Tailscale-adresse ind som prøvedata. Den er ubrugelig uden adgang til tailnettet, men den er stadig et kort over én persons opsætning — og af præcis den grund er arbejdspapiret om flytningen gitignoreret. Så skal en test ikke bære adressen i stedet. En neutral adresse fra samme område prøver det samme. |
||
|
|
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. |
||
|
|
223a1df85e |
ci: checkout prøver begge auth-former og siger hvilken der virkede
Serverens side er bekræftet — Gitea tillader fetch ved hash, målt direkte mod instansen. Tilbage stod kun, om act_runners auto-token duer som kodeord i basic auth, og hvilket brugernavn den vil have. Det kan kun en kørende runner svare på. Feedback-sløjfen går gennem en kø og et andet menneske, så en kørsel, der kun siger 'checkout fejlede', er for dyr. Trinnet prøver nu x-access-token først — den form actions/checkout bruger, og som økosystemet beviser virker mod Gitea — derefter $GITHUB_ACTOR, og skriver hvilken der blev godkendt. Én kørsel besvarer spørgsmålet, uanset om den ender grøn eller rød. |
||
|
|
c85bdd5076 |
ci: CI kan udløses i hånden
Køen på Gitea kan kun ryddes ved at slå Actions fra og til igen, og det dropper alt, også den kørsel man ville beholde. Uden en dispatch-udløser er den eneste vej tilbage at skubbe en tom commit — en løgn om, at der skete noget. Nu kan en kørsel startes mod uændret kode. |
||
|
|
28825dc7cb |
chore: arbejdspapiret om Gitea-flytningen bliver på maskinen
Samme grund som HANDOFF.md: GITEA-FLYTNING.md navngiver kassen, runneren, registret og hvordan man kommer til dem. Det er et kort over én persons opsætning og hører ikke til i et repo, der kan blive offentligt. |
||
|
|
c930e6163e |
ci: Gitea bygger selv — og uden at hente handlinger fra GitHub
DEFAULT_ACTIONS_URL står ikke i app.ini på gitea.nolimit.dk, og standarden er github.com. Et `uses: actions/checkout@v4` i et Gitea-workflow bliver derfor hentet fra GitHub, hver gang, også når både koden, runneren og registret er vores egne — så et workflow, der bruger markedspladsen, ser uafhængigt ud uden at være det. Begge Gitea-workflows er skrevet om uden `uses:`. Værktøjskæden kommer fra jobbets container, hvilket er det, setup-go ellers ville bruge et trin på; checkout, login, metadata og buildx er de rå kommandoer, handlingerne pakker. Det, der ikke fulgte med: rapporten fra en fejlet e2e-kørsel. upload-artifact har ingen `run:`-form, så grunden står i loggen frem for i en zip. Udgivelsen fik samtidig en rettelse, GitHub-udgaven ikke har: en forudgivelse (v1.4.2-rc1) får kun sit eget mærkat. At lade en rc overskrive :latest eller :1 ville sende den ud til alle, der troede, de fulgte det stabile spor. Issue-skabeloner i .gitea/ for sig: Gitea kræver `about`, hvor GitHub skriver `description`, og afviser en skabelon uden. |
||
|
|
20b8c12b4a |
build: imaget hentes fra vores egen Gitea, og runen peger derind
GitHub er utilgængeligt i ti dage. Koden ligger allerede begge steder; det, der manglede, var imaget og et sted at bygge det. **Imaget ligger i Gitea nu**, begge arkitekturer. Kopieret med `docker buildx imagetools create`, som flytter hele manifest-indekset mellem to registre uden at hente lagene ned — en `docker pull` på denne maskine ville have hentet arm64 og efterladt serveren uden en amd64 at køre. **Og det kan hentes uden legitimation, mens repoet bliver ved med at være privat.** Det var ikke givet: panelets `PullImage` sætter aldrig `RegistryAuth`, så et privat register er udelukket, og Gitea gør normalt en container-pakke privat sammen med det repo, den hører til. Men en pakke bindes kun til et repo, når den skubbes med et repo-link — og denne er ikke, så dens synlighed følger brugeren `kw`, som er offentlig. Bevist frem for antaget: `docker --config /tmp/tom pull` hentede den med samme digest som GHCR havde. En bar `curl` mod `/v2/…/manifests/…` svarer 401 også for et læsbart image, fordi registret uddeler et anonymt token først — så den er ikke prøven, og den kostede en runde at gennemskue. **Runen er bumpet til version 5.** Ikke fordi felterne ændrede sig, men fordi en rune ikke opdaterer sig selv: panelet skriver rune-rækken fra en UI-handling, så ændringen kræver **Runes → verdande → Update**, og katalog-listen caches ti minutter. **Et Actions-workflow til Gitea** ligger i `.gitea/workflows/`. Sin egen fil frem for en delt: `secrets.GITHUB_TOKEN` findes ikke der, registret er et andet, og der er ingen Releases-side at skrive til. En delt fil ville være fire betingede led, der hver kun gælder ét af de to steder. Den laver ingen release, fordi opdateringsnotitsen i appen læser GitHubs — skal den flyttes med, er det `internal/update`, der skal have en ny adresse. **Det virker ikke endnu:** der er ingen runner registreret på Gitea. Indtil der er, bygges og skubbes imaget i hånden herfra. Dokumentationen og landingssiden siger den nye adresse. Rune-siden er skrevet om, hvor den fortalte om GHCR-pakkeindstillinger, og siger nu, hvordan man prøver efter på den måde, der faktisk svarer på spørgsmålet. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
c8d67dd07d |
feat(notes): notekroppe forsegles, og søgningen åbner dem selv
CI / OpenAPI is valid (push) Successful in 38s
CI / Go (push) Failing after 18m36s
CI / End-to-end (push) Failing after 18m54s
CI / Docker image builds (push) Successful in 7m27s
Documentation / build (push) Failing after 7m7s
Documentation / deploy (push) Skipped
Release / Build and publish image (push) Failing after 9m59s
Grunden er den samme som for postkassers kodeord og OAuth-tokens: en sikkerhedskopi kan hentes gennem fladen, og indtil nu bar hver eneste af dem hver eneste note i klartekst. Noter er dér, folk skriver kodeord og API-nøgler ned — det ved vi, fordi der ligger sådan nogle i den installation, det her blev bygget til. **Både titel og krop.** Titlen *er* første linje, så en forseglet krop ved siden af en læselig titel ville efterlade begyndelsen af hver eneste note i klartekst. **Og de to kopier gik med.** Det var derfor det ikke var gjort for længe siden: `notes_fts` indekserede kroppen, og den genererede `fold`-søjle bar en translittereret kopi af den. At forsegle kroppen og lade dem stå ville have været teater — en angriber med filen kunne læse hver note ud af indekset. Begge er væk, og tabellen er bygget om, fordi SQLite ikke kan fjerne en genereret søjle på plads. **Søgningen åbner teksten i Go.** Målt frem for gættet: tolv hundrede noter à 3,3 kB koster 33–43 ms pr. søgning, og fladen venter 250 ms efter sidste tastetryk, før den spørger — så det ligger inde i pausen. Tiden går ikke med at åbne konvolutterne; fire megabyte AES-GCM er få millisekunder. Den går med at folde og småskrive hver krop, og det er lineært: regn med omtrent 350 ms ved ti tusind noter. Dér er det den forkerte form, og svaret er da et nøglet indeks — hvis pris er, at præfiks- og delordssøgningen falder bort. bm25 forsvandt med indekset. Det, der er beholdt, er den halvdel, der betød mest: titlen vejer ti gange kroppen. Med et loft pr. ord, så en note, der nævner ordet fyrre gange, ikke slår en, der nævner alle ordene én gang — det er den fejl, en ren optælling laver, og den er let at lave og svær at få øje på. **En bagudfyldning ved opstart, ikke efterhånden.** Instansens indstillinger forsegles, når de skrives, og det er rigtigt for dem. For et arkiv på tolv hundrede importerede noter, som ingen redigerer igen, betyder "når de skrives" aldrig — og det er præcis dét arkiv, der er værd at forsegle. Idempotent, så den koster et gennemløb af id'er hver anden gang, og `updated_at` røres ikke: at lukke en konvolut om en note er ikke at skrive den, og et stempel med i aften ville kaste den rækkefølge væk, importen brugte en dag på at hente hjem. En fejl under bagudfyldningen stopper opstarten. Halvdelen af noterne lukket er ikke i stykker — `v1:`-præfikset gør, at begge slags kan læses — men en instans, der starter og lader som om den beskytter noget, den ikke gør, er værre end en, der siger fra. Hvad der stadig ligger åbent, står i SECURITY.md og på dokumentationssiden: opgavetitler, beskrivelser, kommentarer, og `note_links`, som røber hvilke projekter og opgaver en note peger på, men ikke hvad der står. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
23bd784b07 |
docs: SECURITY.md sagde noget, dagen havde gjort forkert
"Ingen ratebegrænsning ud over login og nulstilling" var sandt i morges. Det er det ikke længere: andenfaktoren, beaconet og indgående post har fået deres eget loft i løbet af dagen — det sidste tilføjede jeg selv og skrev ikke det her. En SECURITY.md, der siger noget forkert, er værre end de fleste fejl. Den er netop den fil, nogen læser for at afgøre, om de tør køre det her, og hvert eneste punkt i den er en påstand, de ikke selv kan efterprøve uden at læse koden. Samme sted mangler den største åbne ting: notekroppe, opgavetitler, beskrivelser og kommentarer er ikke krypteret. Forseglingen dækker legitimation — postkassers kodeord, OAuth-tokens, andenfaktorens hemmelighed, AI-nøgler — og intet andet. En note med et kodeord i har det i klartekst i hver sikkerhedskopi, man kan hente. Det stod i overleveringen og på dokumentationssiden for noter, og det hørte hjemme her. README's "ikke færdigt"-liste havde Gmail med. Den kører i produktion, inklusive flagningen og dubletbehandlingen, der tog en dag at få rigtig. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
264f48ae11 |
fix(quickadd): "hver anden tirsdag" blev til en dato og to ord i titlen
Release / Build and publish image (push) Failing after 10m3s
Vendingen er den, folk bruger, og recurrence-pakken har kunnet den siden den blev skrevet — den oversætter "hver anden" til "hver 2." netop for at slippe for at skrive hvert eneste mønster i to udgaver. Sætningen nåede bare aldrig frem til den. Mønsteret i hurtig tilføjelse tog et tal efter "hver", men ikke et ord, så det, der blev givet videre, var "hver anden". Det er ikke en regel. Så blev de to ord stående i titlen, "tirsdag" blev læst som en forfaldsdato, og opgaven gentog sig ikke — uden at noget sagde det. Sløjfen, der prøver sig frem, kunne ikke redde den: den fjerner ord fra enden, én ad gangen, og kan ikke hente et ord, mønsteret aldrig tog med. Fundet af en prøve, jeg skrev til noget andet, hvor jeg brugte vendingen som en selvfølge og opdagede, at den ikke var det. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
f64b7c1f01 |
docs: Noter og Kalender havde ingen sider, mens forsiden lovede begge
Landingssiden reklamerer for noter og for en kalender, og dokumentationen havde hverken en side om den ene eller den anden. Det hul blev skrevet ned i morges og er det her. **Noter** siger, hvad der er værd at vide, og hvad der ikke virker: at første linje *er* titlen og udledes ved hver gemning, at arkivet og papirkurven er to forskellige ting, at søgningen finder `grøn` når man skriver `gron` og `keepInventory` når man skriver `inventory` — og at notekroppe ligger i klartekst i hver sikkerhedskopi, man kan hente. Det sidste hører hjemme dér, hvor nogen står med en note og overvejer at skrive et kodeord i den. **Kalender** samler det, der før var spredt: gitteret, ugen som et døgn, træk der sætter et klokkeslæt, abonnementer på ICS og webcal, og Google-forbindelsen med det forbehold, den skal have — den er skrevet efter dokumentationen og har aldrig kørt mod Google. `caldav.md` er nu den anden retning alene: verdandes egne opgaver læst af noget andet. En ting, byggeriet lærte: `</>` i inline-kode vælter Pythons html-parser på 3.9 med "we should not get here!". CI kører 3.12 og er ligeglad, men en side, der ikke kan bygges lokalt, er en side ingen retter. Knappen er beskrevet med ord i stedet. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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 <[email protected]>
|
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]>
|
||
|
|
e29fd04abd |
docs: noternes opbygning, skrevet til at blive løftet ud
Hvordan noter er bygget her: modellen, syntaksen, søgningens tre lag, editorkontrakten, vedhæftninger, ind- og udførsel — og begrundelserne, som er den del, der ikke kan læses ud af skemaet. Den begynder med fjorten spørgsmål, der skal besvares før der skrives kode, og siger det til den, der læser den, med rene ord: gæt ikke en standard. Grunden står i filen — omtrent en tredjedel af designet her er arvet fra de omgivelser, noterne bor i, og løftet ind i et program med andre omgivelser uden at afgøre hvad der skal med, giver noget, der virker en uge og derefter slås med én. Den siger også, hvad der ikke virker: notekroppe i klartekst i hver sikkerhedskopi, import der aldrig fletter, ingen sync. En specifikation, der kun beskriver det, der virker, lyver ved fortielse. I repoet frem for uden for det, som HANDOFF.md og NOTES-PLAN.md er. De to er arbejdspapirer om *dette* program; den her er beskrivelsen af en del af det, og den bliver kun forkert, hvis koden flytter sig uden at den følger med — hvilket er lettere at opdage, når den ligger ved siden af. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
be581eb8ae |
fix(search): et ord inde i et ord kunne ikke findes
Release / Build and publish image (push) Failing after 9m57s
Meldt ind som "søgning har ikke det store held": en note med `gamerule keepInventory true` kunne ikke findes ved at skrive "keep inventory". Et indeks er bygget af tokens, og et token er det, en tokenizer besluttede var et ord. `keepInventory` er ét. Så "keep inventory" spørger efter to ord, der ikke findes nogen steder, og søgningen svarer, at der ikke er noget — mens noten står lige der. Præfikssøgningen tager halvdelen af det. `keep*` rammer faktisk `keepinventory`; `inventory*` kan ikke, for det er ikke et præfiks af noget. Og de to led er bundet sammen med AND, så den halvdel, der virker, bliver kasseret af den, der ikke kan. Det er ikke et sært tilfælde i en notesapp. Ti års noter er fulde af kommandoer, identifikatorer og filnavne, og hver eneste af dem er flere ord, indekset tror er ét: `keepInventory`, `VACUUM INTO`, `apple-notes-til-markdown`. Indekset svarer først. Har det ingenting, læses teksten: hvert ord skal stå et sted i noten, hvor som helst, i hvilket som helst ord. `fold` er `title || ' ' || body` med de danske bogstaver translittereret, så én søjle dækker titlen, brødteksten og "gron" for "grøn" på én gang. Opgavesøgningen har fået den samme arm. At have den i den ene af de to ville være værre end at have den i ingen af dem: "den finder det i mine noter, men ikke i mine opgaver" er den næste melding, og svaret på den ville være denne linje. Hvad det koster, og hvornår formen er forkert, står skrevet ved koden: en LIKE med et indledende % kan ikke bruge et indeks, så den læser hver note. Ved tolv hundrede noter er det fire megabyte og et par millisekunder, og den kører kun, når den hurtige vej allerede kom tomhændet tilbage — altså netop den søgning, der var ved at skuffe nogen. Ved hundrede tusind er svaret et FTS5-`trigram`-indeks, ikke en større scanning. Prøverne er set fejle uden rettelsen, og de dækker begge veje: "inventory" alene, som ingen præfikssøgning kan nå, og "Bedrock", som stadig skal besvares af indekset — ellers er rangeringen, der blev bygget til tolv hundrede noter, stille forsvundet. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
4b3abc44fb |
feat(site): hvad der er landet, og hvad man tager på sig ved at køre det
To dele. **Lately** siger, hvad der er kommet til, fem linjer med versionsnummer i egen søjle. En landingsside kan påstå, at et projekt er i live; en liste over hvad der faktisk landede siger det bedre. Ikke en changelog — den ville gå i stå — så den peger på release-noterne, som skrives alligevel. **At your own risk** siger det, MIT-licensen siger, og som ingen læser: uden garanti af nogen art. Dertil det, der er sandt om netop dette: én persons projekt, version 0.x, du hoster det selv, så data og sikkerhedskopier er dine — og en sikkerhedskopi, ingen har gendannet, er ikke en sikkerhedskopi endnu. At der er lavet to gennemgange af koden, at hvert fund er skrevet ned og lukket, og at **ingen udefra har auditeret det**. Sat i samme skriftstørrelse som alt andet på siden. Et forbehold med mindre skrift end resten er skrevet for at kunne peges på bagefter, og det er den ene ting, siden ikke skal gøre. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
cab4e703f5 |
fix(sikkerhed): CalDAV lod en viewer skrive, og Web Push kunne ringe indad
Release / Build and publish image (push) Failing after 10m0s
To huller fundet ved en gennemgang efter dagens arbejde. Begge har en prøve, der er set fejle før rettelsen. CalDAV PUT afgjorde adgang på det projekt, klienten *nævnte i adressen*, og tog derefter opgavens id fra den samme adresse. Intet bandt de to sammen. En viewer på et delt projekt kunne pege på et projekt, de selv ejer — indbakken, som alle har og alle må redigere — og lægge den delte opgaves id i filnavnet. Projekt- tjekket gik godt på indbakken, og skrivningen landede på en opgave, de kun må læse. GetTask lukkede det ikke: den svarer "må denne person se det", og det må en viewer. `CanEdit` er spørgsmålet, og det er det samme, handleUpdateTask altid har stillet — API-døren har spurgt hele tiden, CalDAV-døren spurgte ikke. Opgaven skal nu også ligge, hvor adressen siger. Uden det kan en redaktør på to projekter nå det ene gennem det andets adresse; intet eskaleres af det i sig selv, men en adresse, der har lov at lyve om hvor noget bor, er sådan sagen ovenfor kom ind. Web Push blev leveret med en almindelig http.Client. Adressen kommer fra browserens abonnement — altså fra den, der er logget ind — og serveren POSTer til den på et skema, de selv vælger: sæt en påmindelse et minut ude, og jobbet går igen hvert minut. Tilmeldingen afviser en privat adresse, og den kontrol er ikke nok alene: den kører på URL'en som skrevet, og et navn, den godkender, kan svare 127.0.0.1 næste gang det slås op. Det er præcis den sætning, `internal/safedial` selv står og advarer om. Leveringen går gennem safedial nu, som tjekker den opslåede adresse ved forbindelsen og hvert viderestillingshop på samme måde. Kontrollen ved tilmelding bliver, fordi den siger nej, mens nogen kigger på skærmen. Og en hærdning: /inbound/mail har fået et loft. Det er den svagere af de to offentlige skriveveje — beaconet skriver en række for en fremmed, denne skriver ingenting uden en 256-bit adresse, man skal kende i forvejen — men adressen deles med mailservere og står på en indstillingsside, og når den først er kendt, var der intet loft på, hvad der kunne laves med den. Tallet er sat generøst: modtageren er mailserveren, én adresse der leverer det hele. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
43f8c0ea85 |
feat(site): landingssiden siger hvad der findes nu, og siger det med kanter
Release / Build and publish image (push) Failing after 10m0s
Siden stod fra før kalenderen, noternes billeder og arkiv, sikkerhedsarbejdet og beaconet. Nu står de der: søgning der læser dansk, kalenderen, Venter på andre, gentagelser, AI slået fra som udgangspunkt, andenfaktor, at ingen skriver sig selv op, og beaconet — sagt som det er, tændt fra start og med ét klik til at slukke. Google Kalender står med sit forbehold på selve kortet. Den er skrevet efter dokumentationen og har aldrig kørt mod Google, og en landingsside, der siger noget andet, er den ene sætning, resten af siden ikke kan bære. Ikke-målene løj om posten: "No mail integration beyond Gmail and the forwarding address" har været forkert, siden IMAP kom til. iCloud og Fastmail virker. Det, der faktisk ikke virker, er Outlook, og det er nu det, der står. Designet: en bjælke der bliver stående, fordi siden er 5.600 px og vejen ud kun lå i toppen. Fire fakta under knapperne, hvor en landingsside plejer at sætte logoer, den ikke har gjort sig fortjent til. Kort med egne kanter frem for celler i ét gitter — ti celler i tre søjler er et hul i tegningen, atten kort i tre grupper er ikke. To paneler under "Running it", fordi de er alternativer og ikke trin. Ikke-målene som en liste, der kan tælles. Og tre skærmbilleder, der lå i site/screenshots uden at være brugt. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
6b4448b245 |
fix(settings): AI, Gmail og kalenderen delte én række og slettede hinanden
Meldt ind som "AI-informationerne gemmes ikke". De blev gemt. De blev taget bagefter. `user_settings` blev oprettet med `user_id` som hele sin primærnøgle — én række per person, uanset scope — og upserten skrev `scope` om ved konflikt. Der er tre scopes i live: `ai`, `gmail` og `calendar`. De delte den ene række, så hver skrivning slettede de to andre, og hver eneste svarede 204. Det virker begge veje: at forbinde kalenderen slettede Gmail-forbindelsen, og Gmail slettede kalenderen. Fælden var set komme to gange. 0011 gav instansens indstillinger deres egen tabel delvis af den grund og skrev advarslen ned med rene ord; 0014 flyttede postkasserne ud og skrev, at advarslen "nær var trådt i for anden gang". Tredje gang var kalenderen, som lagde et `calendar`-scope ved siden af de to andre. Lektien værd at holde fast i: en advarsel i en migration bliver læst af den, der redigerer *den* migration, og det var ingen. Formen er rettet her, så der ikke er noget tilbage at advare om — `PRIMARY KEY (user_id, scope)`, og upserten rammer den nøgle. Tabellen bygges om, fordi SQLite ikke kan ændre en primærnøgle på plads. De rækker, der findes, kopieres som de er: hver person har højst én, og den holder det scope, der vandt sidst. Det, de tidligere skrivninger tog, er overskrevet og kan ikke hentes tilbage her — efter opdateringen skal AI-udbyderen sættes op igen, og Gmail-forbindelsen tjekkes. To prøver: én mod store-laget, hvor formen var fejlen, og én gennem handlerne, hvor den blev set. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
4aa8bd6a40 |
fix(sikkerhed): resten af gennemgangen — SSRF, tre ubegrænsede veje, og fire hemmeligheder
Release / Build and publish image (push) Failing after 9m54s
**H2 — SSRF gennem AI-udbyderens adresse.** `base_url` blev gemt uden kontrol og hentet med en almindelig klient, og udbyderens svar blev givet tilbage ordret. Det var en fungerende skanner af det interne net for enhver bruger: peg den på en vært, læs de første tre hundrede bytes af det, der svarede. På en homelab er det det meste af, hvad der er værd at nå. Nu gennem `safedial`, og svarets krop går i loggen frem for i svaret. Operatøren har stadig brug for beskeden — for en lokal model er den som regel selve problemet — så den er der, bare kun for den, der kører instansen. **M2 — Web Push havde tre.** Endepunktet blev gemt ukontrolleret og POSTet til på en tidsplan, den, der ringer, selv vælger: sæt en påmindelse et minut ude, og serveren henter adressen, hvert minut. Upserten flyttede desuden ejerskabet med `user_id = excluded.user_id`, så enhver, der kendte en andens endepunkt, kunne overtage det — offerets telefon ville få angriberens beskeder og ingen af sine egne. Og `DeletePushSubscription` havde ingen ejer-kontrol. **H3 — tre veje uden loft.** FTS-udtrykket byggede omkring halvtreds bytes pr. ord uden grænse; en søgning på en kvart megabyte blev til to megabyte MATCH-udtryk og seksogtyve sekunder inde i SQLite, mens den holdt på én af kun fire til otte forbindelser. Nogle få af dem samtidig, og databasen er utilgængelig for alle på instansen, logins inklusive. `filterquery.Parse` er kvadratisk — halvtreds tusind led er syv sekunders CPU og seksogfyrre gigabyte allokering i én forespørgsel — og havde hverken loft på antal eller på dybde. Og noteimporten læste hver fil i arkivet uden grænse: 64 MB komprimeret kan blive til seksogtres gigabyte skrevet ned på den disk, databasen ligger på. **M1 — `comment_id` blev taget på tro.** Rettighedskontrollen så på opgaven i adressen, mens filen blev hængt på den kommentar, formularen bad om. En viewer på et delt projekt kan liste dets kommentarer, så hun kunne læse et id og lægge en fil under en andens kommentar i et projekt, hun kun må læse. **M4 — fire hemmeligheder lå i klartekst i hver sikkerhedskopi.** TOTP-koden og AI-nøglen forsegles nu; begge konverterer sig selv næste gang de skrives. Kalenderfeedets og mailadressens tokens gør ikke — de slås op på deres præcise værdi og vises i fladen mere end én gang, hvilket en hash ikke kan. Det står nu skrevet i `SECURITY.md`, som påstod det modsatte. At rette det betyder, at adressen kun kan vises én gang, og det er en beslutning om, hvordan funktionen virker. Og en prøve på, at politikken overhovedet bliver sendt. Det var den, der gjorde XSS-en i nat til indsat HTML frem for kørt script, og der var ingen prøve, der rørte den — en foranstaltning, ingen prøve rører, kan forsvinde i en oprydning, uden at nogen opdager det. Målt på script-src alene: `style-src 'unsafe-inline'` står der med vilje, og en indsat stil kan gøre en side grim, ikke køre kode. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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. |
||
|
|
9d743a5d46 |
fix(tools): shots skrev PNG'er ned i mappen med WebP
Standardstien var site/screenshots, som siden landingssiden gik over til WebP kun indeholder WebP. En kørsel lagde tolv PNG'er ved siden af dem, som ingen tekst peger på. Den skriver nu til docs/, hvor de andre PNG'er ligger. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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. |
||
|
|
71a9fe63d6 |
docs: noterne findes også i teksten, og hvert billede har et sted at høre til
README'ets billeder havde været brudte siden landingssiden gik over til WebP: site/screenshots/*.png blev slettet dengang, og markup'en pegede stadig på dem. Den peger nu på PNG'erne i docs/, som er dem, der findes. Noter er en af de største ting i appen og stod ikke ét sted i teksten — hverken i README'et, på landingssiden eller i dokumentationen. Nu gør de, med de to nye billeder. Fem billeder pegede ingen tekst på: upcoming, sections, delegated, settings og admin. De har fået et afsnit i docs/index.md, hvor hvert enkelt beviser noget, siden siger. Landingssidens gitter viste den lyse udgave to gange — den ene af dem er blevet til noterne. Og to billedtekster passede ikke længere på billedet: temaerne viser nu også de fire udseender, og opgaven i skuffen viser ikke sin samtale. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
1b1e57df29 |
docs: kalenderen den anden vej, og én registrering til begge Google-ting
`caldav.md` beskrev to måder at sende opgaver *ud*. Den tredje er den modsatte retning og hører til samme sted, fordi det er dér, man ville lede. `configuration.md` hed "Gmail" og handler nu om Google: samme klient, to redirect-URI'er, to API'er, to scopes. Bemærkningen om en Internal-app står begge steder, fordi den gælder begge veje — og for én privat konto er kalenderens egen hemmelige ICS-adresse den kortere vej. Intet i verdande abonnerer på sådan en endnu. |
||
|
|
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 <[email protected]> |
||
|
|
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. |
||
|
|
241c5ed37d |
fix(mail): den samme mail blev en opgave igen og igen
Release / Build and publish image (push) Failing after 9m58s
Rapporteret fra drift med et skærmbillede af den samme mail seks gange i træk. **IMAP: flaget kommer af, når mailen er blevet en opgave.** Markøren var et uid — husk det højeste, begynd over det næste gang — og den holdt kun, hvis uids var, hvad serveren sagde. Den brød på et fetch-svar uden uid, på en UIDVALIDITY, der skiftede under en genopbygget postkasse, og på to postkasse-rækker, der pegede samme sted og hver især førte deres eget regnskab. Alle tre gav det samme symptom. Så regnskabet flytter derhen, hvor spørgsmålet bliver stillet. "Flaget" betyder "lav det her om til en opgave"; når den er en opgave, har flaget gjort sit arbejde og er ikke sandt længere. Så er der ingenting at huske, og dermed ingenting, der kan komme ud af trit — en anden synkronisering, en anden postkasse-række eller en database gendannet fra en kopi finder alle det samme svar, fordi svaret ligger i postkassen. Flaget tages af *efter* opgaven er skrevet. Et flag fjernet fra en mail, hvis opgave aldrig blev gemt, er en mail, ingen ser igen. **Gmail havde den samme fejl af en anden grund.** Listen over allerede behandlede beskeder blev bygget om ved hver kørsel af *kun de id'er, Gmail svarede med den gang* — for at den ikke skulle vokse uendeligt. Men forespørgslen er begrænset, så på en konto med mere stjernemarkeret post, end der er plads til i ét svar, glemte hver kørsel det, den forrige havde husket. De kom tilbage som ubehandlede og blev opgaver igen. Det var ikke en grænse, det var en måde at glemme på. Listen har nu et loft på antal i stedet, nyeste først. Gmail kan ikke få taget stjernen af: scopet er `gmail.readonly`, med vilje, fordi det er dét, der gør samtykkeskærmen til noget, et menneske kan sige ja til. Derfor skal regnskabet blive på vores side dér — men det skal så være et regnskab, der kan holde. Prøven kører hele vejen mod en rigtig IMAP-server: find de flagede, tag flaget af, og spørg så forfra med markøren sat til nul — altså det værst tænkelige tilfælde, hvor alt, vi selv havde husket, er væk. Der kommer ingenting tilbage. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
7828c1e6ed |
feat(gcal): Google Calendar kan læses, og kalenderen får sine egne tabeller
Klienten henter kalenderlisten og begivenhederne i et vindue, og laver Googles to former om til den ene, et gitter kan spørge om: dage i begge ender, begge med. Googles heldags-slutdato er eksklusiv, og et møde, der slutter ved midnat, slutter aftenen før — begge dele rettes ét sted frem for hos hver læser. Egne tabeller frem for `mailboxes`. De to ligner hinanden på afstand og er uenige om hver eneste kolonne bagefter: `trigger_kind`, `seen`, `last_uid` og `project_id` svarer alle på "hvad laver en besked om til en opgave", og intet her bliver til en opgave. Én konto rummer desuden mange kalendere, og en en-til-mange er der ikke plads til. Det afgørende er mindre og mere praktisk: `mailboxes.kind` har en CHECK, og SQLite kan ikke ændre en CHECK — en tredje værdi koster en tabelombygning med fremmednøgler slået fra. Derfor er der ingen CHECK på `provider` her: CalDAV skal koste en række, ikke en ombygning. |
||
|
|
a46d9158e1 |
refactor(google): OAuth-flowet flytter ud af gmail, så to ting kan dele det
Gmail og Google Calendar logger ind gennem den samme registrering med den samme PKCE og den samme fornyelse. Det eneste, der adskiller dem, er ét ord: scopet. En kopi mere ville være et sted mere, hvor `access_type=offline` kan blive glemt — og det er dén fejl, der får en forbindelse til at dø lydløst en time efter, den blev lavet. internal/gmail beholder sit API og sit scope; navnene på flowet er aliaser, så hverken kaldere eller prøver ændrer sig. Prøverne for PKCE, AuthURL og Configured flytter med koden. |
||
|
|
4b5f3f0864 |
chore: agenternes arbejdstræer hører ikke i repoet
Release / Build and publish image (push) Failing after 9m56s
Et `git add -A` fangede .claude/worktrees/ som et indlejret repo. Det er en agents arbejdskopi og skal ingen steder. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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 <[email protected]>
|
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
2ffbd9533b |
fix(notes): billederne i en importeret note pegede på ingenting
Release / Build and publish image (push) Failing after 9m54s
Fundet ved at importere rigtige noter frem for ti prøvenoter. Linket blev skrevet halvt om. attachToNote kendte to måder, en note kan nævne en fil på — den bare filnavn og stien i forhold til noten — men ikke den, en Apple Noter-eksport faktisk skriver: `vedhaeftninger/751-2-IMG_0615.heic`, med noten i roden ved siden af mappen. Kun det bare navn matchede, så kun det blev erstattet, og hver eneste billede i hver eneste importerede note endte som `vedhaeftninger//api/v1/attachments/…`, som er ingenting. Filerne var gemt og hæftet rigtigt på; det var linket, der var forkert. Længste match først, ellers bliver mappen stående foran adressen. Og billederne kunne alligevel ikke vises. Alt blev serveret som application/octet-stream med Content-Disposition: attachment, hvilket er en god regel med en god grund — en uploadet SVG eller HTML vist inline ville køre sit eget script på dette domæne med sessionscookien på. Men en note bærer sine billeder i teksten som ``, og under den regel er hvert eneste af dem et brudt billede. Hele importen findes for at tage dem med. En kort liste af rasterformater vises nu inline med deres egen type. Det er ikke en opblødning: listen er navngivet ét format ad gangen frem for matchet på "image/", fordi præfikset ville lukke image/svg+xml ind — et dokument, der kan køre script — og en liste, der vokser ved et uheld, er præcis sådan det sker. nosniff står, så browseren skal læse filen som netop det billede eller slet ikke. Titlen stod to gange i fjorten noter. Noter kalder noten det, der står på de første linjer sat sammen med mellemrum, så en note, hvis to første linjer er "CPU" og "CAN", hedder "CPU CAN" — og sammenligningen med kun den første linje gik fri. Overskrifterne øverst tages nu med, så længe de tilsammen stadig er begyndelsen på titlen. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
43ad60a84a |
feat(notes): formateringen overlever, og monospace bliver til en terminal
Konverteringen læser nu Apple Noters HTML frem for at flade den ud med textutil. Overskrifter, fed, kursiv, understreget, lister og monospace kommer med. En note er ikke kun sine ord — et referat med overskrifter er ikke det samme referat som en klump — og ti prøvenoter viste hvad det kostede. Skrevet om til Python, fordi HTML skal læses og ikke gættes med regulære udtryk. Den alvorligste fejl, prøvekørslen fandt: navn og krop blev hentet i hvert sit kald, og Noter sorterer listen efter ændringsdato. Flyttede noget sig imellem de to kald, fik en note én titel og en andens indhold. På ti noter er det synligt; på tolv hundrede ville det være ubemærket rod. Der hentes nu ved id, og navn og krop kommer i samme kald. Sammenhængende monospace-linjer bliver til én blok. Hver linje i Noter er pakket ind i sin egen div, hvis starttag tømmer den forrige — så mellem to tt-linjer kom der altid en tom flush forbi, og hver linje i en terminaludskrift blev sin egen blok. Og i editoren ser en kodeblok ud som en terminal: mørk flade, monospace, sproget i hjørnet, farver på strenge, tal, kommentarer, nøgleord og prompten. Monospace ER kode i praksis, og en mørk flade siger det på afstand. Farverne lægges på de blokke, markøren IKKE står i. At farve mens nogen skriver betyder at bygge indholdet om ved hvert tastetryk og sætte markøren tilbage bagefter — det virker ni gange ud af ti og flytter markøren midt i en sætning den tiende. Et API-token er ét ord, der er bredere end skærmen. Uden min-width: 0 skubbede en sådan note hele listen bredere og klemte editoren sammen — på en note, man ikke engang havde åben. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
08fc355bf9 |
fix(tools): tre fejl, en prøvekørsel på ti noter fandt med det samme
Kodningen. textutil gætter på inddataets kodning og gætter forkert, så "Ansøgning" kom ud som "Ansøgning". Det ser ud som én underlig note, indtil man opdager, at det er hver eneste note med et dansk bogstav i. Titlen stod to gange. Kroppen af en Apple-note begynder med notens egen titel, så en overskrift ovenover gav den dobbelt overalt. Og listerne var ikke lister. Apple skriver dem som tabulator, tegn, tabulator — en liste for et menneske og ingenting for en Markdown-læser, så en huskeliste ankom som en klump linjer. Punkttegnet er flerbyte, og BSD sed hakkede det midt over, så hver linje begyndte med to halve tegn; perl med -CSD gør det rigtigt. Scriptet tager nu et antal som andet argument, så en prøvekørsel er til at gentage. Det er billigere at opdage den slags på ti noter end på tolv hundrede. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]>
|
||
|
|
f6c60bdaf9 |
fix(notes): #garageristeriet og #GarageRisteriet er samme projekt
Release / Build and publish image (push) Failing after 9m58s
De var det ikke. Nøglen blev gemt præcis som skrevet og slået op på samme måde, så en note, der nævnte et projekt med lille begyndelsesbogstav, dukkede simpelthen ikke op på det — stille, og uden noget der antydede hvorfor. Foldet på vej ind og foldet i opslaget, så sammenligningen bliver ved med at være en nøjagtig lighed og bruger sit indeks. Havde man i stedet sænket kolonnen i forespørgslen, skulle hele tabellen læses. Kun projekter. En [[notetitel]] er en titel, ikke en nøgle: to noter, der hedder "Møde" og "møde", er to noter, og at folde dem sammen ville slå noget sammen, nogen havde holdt adskilt. Der er en prøve for begge dele. Migrationen samler de rækker, der allerede er skrevet. Primærnøglen er (note_id, kind, target_id), så en note, der skrev begge stavemåder, ville kollidere med sig selv — derfor bygges sættet forfra med INSERT OR IGNORE frem for en opdatering, der ville fejle på parret. Fladen viser projektets egen stavemåde og ikke den foldede nøgle. En etiket, ingen har skrevet, og som står hverken i noten eller på projektet, ville være værre end ingen. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
3223989a9d |
feat(notes): første linje er titlen, resten er brødtekst
Release / Build and publish image (push) Failing after 9m57s
En ny note åbner på sin titel, og linjen efter er brødtekst — uden at man vælger noget. Det er det, Apple Notes gør, og det er også hvad listen har brug for: første linje bliver notens navn, så en note, der begynder i brødtekst, hedder sin egen første sætning. To ting undervejs. Reserven for en tom note blev aldrig brugt, fordi en tom tekst ikke gengives som ingenting men som ét tomt afsnit — den prøves nu på teksten og ikke på resultatet. Og Chromium svarer på retur med et bart <div>, som læses som brødtekst men ikke er formateret som det: afstanden mellem afsnit forsvandt stille i det øjeblik, man trykkede retur frem for at vælge Brødtekst i menuen. Listen viste desuden den rå Markdown i forhåndsvisningen — "# Møde med Anders" og "**fed**" — hvilket er præcis det, editoren blev bygget for at holde op med at vise. Den viser nu sætningen frem for notationen. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
f557d319ca |
feat(sidebar): de faste visninger kan sorteres, og rækkefølgen følger kontoen
Release / Build and publish image (push) Failing after 9m57s
I dag, Kommende, Venter og Indbakken kan trækkes i den rækkefølge, man vil have dem. På kontoen frem for i browseren: sidebjælkens bredde er en kendsgerning om skærmen, og svaret på en telefon er ikke svaret på en skærm — men hvilken rækkefølge man vil have dem i, er en kendsgerning om personen. En liste af nøgler frem for et tal pr. visning. Ukendte nøgler droppes og manglende føjes til sidst, så en visning kan tilføjes eller fjernes uden at strande nogens rækkefølge — hvilket allerede er sket én gang, da Færdige forlod menuen. To ting, prøverne fandt undervejs, og begge var mine: Rækken som trækgreb gjorde alt indeni trækbart, og sidebjælken er allerede modtager for opgaver — en opgave trukket til I dag holdt op med at lande, fordi rækken havde taget gestussen. Et lille greb tager ingenting, før man griber det. Og drop-håndteringen slugte begivenheden, før den tjekkede, om den overhovedet var en menuflytning. Migration 0019. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
5757f06e7a |
feat(notes): man kan se hvad man skriver
Release / Build and publish image (push) Failing after 10m0s
Formatering vises, mens man skriver. Overskrifter, fed, kursiv, kode, citater, punkter, #tags og [[links]]. Et spejl bag et gennemsigtigt tekstfelt — samme greb som hurtig tilføjelse bruger til at vise, hvad parseren forstod. En contenteditable ville lade teksten style direkte og ville også ødelægge fortryd, autokorrektur og IME. Det er en dårlig byttehandel i det ene sted i programmet, hvor nogen skriver en halv time i træk. Mærkerne bliver stående, dæmpede. Obsidian skjuler dem uden for den linje, markøren står i; at dæmpe dem altid er en mindre idé, der ikke flytter tekst under markøren mens den bevæger sig — og den overlever mødet med et tekstfelt. Overskrifter skifter vægt og farve, aldrig størrelse: en større skrift ville fylde flere linjer end tekstfeltet gør, og så skred alt nedenunder. Hele risikoen ved grebet er, at de to lag skal flugte tegn for tegn. Prøven måler det frem for at stole på det: skrifttype, linjehøjde, ombrydning, polstring og den faktiske højde af de to lag med samme tekst i. Undervejs: kursiv-passet slugte " og " efter et fed-udtryk, fordi den løse stjerne stadig var synlig for det. Hvert pas læser nu en kopi, hvor de tidligere pas' tegn er blændet ud. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
fbfc98e217 |
feat(notes): noter kan nås — listet, søgt og fundet baglæns
Anden etape. Handlere, ruter og OpenAPI. Adgang følger projektet, for det er der, noten bor: en note i et delt projekt kan læses af alle, der kan læse projektet, og ændres af en redaktør. En note uden projekt er sin forfatters alene — samme svar som en opgave i indbakken får, nået frem til på samme måde frem for ved en regel, noter fandt på for sig selv. Listning og søgning er samme endepunkt, fordi de svarer på det samme spørgsmål med forskellige ord. Filtreringen sker EFTER indekset: FTS5 svarer om tekst, og at lære indekset om roller ville lægge en adgangsregel et sted, ingen ville kigge den dag den var forkert. Der er en prøve for, at en fremmeds søgning ikke finder noget, og at en fremmed får 404 og ikke 403 — så et id ikke kan afprøves. Den baglæns rute hænger på det, der spørges om: et opgave-id giver hver note, der nævner den; et projektnavn giver hver note med det #tag. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
98ea08b0bb |
feat(notes): fundamentet — Markdown, søgning og links begge veje
Release / Build and publish image (push) Failing after 10m3s
Første etape af noter. Tabel, CRUD, fuldtekstsøgning og de links, en note bærer. Markdown i en kolonne, ikke rich text og ikke et format af vores eget. Eksport var et krav, før der var skrevet en linje, og er den gemte form den eksporterede form, er der ingen konvertering tilbage at tabe noget i — noten kan læses af enhver anden editor den dag programmet ikke findes. Et #tag i en note er DET SAMME tag som i en opgave. Ikke en anden slags, der ligner. Det er hele svaret på "tag en note og se den i projektet": referencen er den samme, så der ikke opstår to systemer, brugeren selv skal holde i sync. Links gemmes som rækker, udledt af teksten ved hver gemning. Teksten er stadig sandheden; tabellen er et indeks over den. Uden det ville "hvad peger på denne opgave" betyde en gennemsøgning af al prosa, og den rude ville være for langsom til at lade stå åben. De rives ned og bygges op frem for at blive sammenlignet: en sammenligning er en anden beskrivelse af samme kendsgerning, og den kan komme til at være uenig med teksten. Noter ligger under projekter og arver deling, roller, papirkurv og historik. En mappestruktur ved siden af ville være ét hierarki for meget. MatchExpr fik navngivne kolonner. Den var skrevet til opgavernes og gav "no such column" mod noternes — hvilket i det mindste er højlydt. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
c44099cac0 |
feat(site): vælg lys eller mørk, og skærmbilledet følger med
Siden fulgte systemet og kunne ikke andet. Nu er der en knap, valget huskes, og skærmbilledet øverst skifter med — der ligger både en mørk og en lys udgave af det samme, og det er kejtet at vise et mørkt skærmbillede på en lys side. Farverne står to gange med vilje. Mediaforespørgslen er hvad en førstegangsbesøgende får; attributten er hvad en, der har valgt, får — og den skal vinde begge veje, både et valgt lyst tema på et mørkt system og omvendt. Én regel kan ikke gøre begge dele. Begge skærmbilleder ligger i markuppen og skjules med CSS, så ingen af dem skal hentes i det øjeblik nogen trykker på knappen. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
86ec0ce2d9 |
fix(imap): markøren er vores, ikke serverens
Samme mail blev en opgave hver tiende minut, og markøren stod stille på 1271. Målt i produktion: en manuel synk lavede én opgave og flyttede ikke last_uid en eneste plads. Årsagen er ikke vores regnestykke, men hvad søgningen svarer. Vi beder om 1272:* — og en rigtig server læser det som 0:1272 og sender hele postkassen tilbage. Alt, hvad den finder, ligger så under markøren, og markøren kan derfor aldrig komme videre. Uid'er ved eller under markøren frasorteres nu hos os. Det koster ét gennemløb af nogle hundrede tal og afhænger ikke af nogen andens læsning af specifikationen. De to tidligere forsøg var ikke forkerte, de var bare ikke årsagen: at tage uid'en fra søgningen frem for fra fetch-svaret, og at give hver postkasse sin egen lås, var begge reelle fejl. Denne er den, der fik den til at gentage sig. Prøven læser en postkasse med markøren over alt i den og kræver nul tilbage — og gentager det for hver enkelt besked. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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 <[email protected]> |
||
|
|
997dffe01c |
docs: skriv det, der blev bygget, og ret det, der ikke passer længere
To nye sider. Postkasser: at stjerne i Gmail eller flage i iCloud og Fastmail bliver til opgaver, at hver postkasse hører til den, der forbandt den, at app-kodeord er noget andet end ens rigtige, og at Office 365 ikke kan nås ad den vej — sidstnævnte skrevet ned frem for at blive opdaget ved at prøve. Og at logge ind: kodeord, totrinskode og passkeys, med hvorfor en passkey ikke kan laves på en bar IP-adresse. Konfigurationen manglede VERDANDE_SECRET_KEY, som er den, det koster mest ikke at vide noget om: mister man nøglen, skal postkasserne forbindes forfra. Begrundelsen står med — det er kopierne, ikke serveren, der er problemet, for de kan hentes gennem fladen. Og genvejsafsnittet i quick-add beskrev q, t og u, som ikke findes længere. En dokumentation, der lover taster, der ikke gør noget, er værre end ingen: man prøver, der sker intet, og så tror man appen er i stykker. Bygget med --strict; alle krydshenvisninger rammer. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
d02d8edfe9 |
feat(site): klik et skærmbillede og se det i læsbar størrelse
Release / Build and publish image (push) Failing after 10m5s
Et skærmbillede af en brugerflade siger ingenting ved 281 pixels, og det er der, de fleste af dem sidder i gitteret. Et klik åbner nu det samme billede omkring tre en halv gang større. <dialog> frem for et selvbygget lag: Escape lukker, fokus bliver inde i den, resten af siden bliver inert — tre ting browseren gør gratis og som et selvbygget overlay skal have kode for hver af. Billedet får rolle og tabindex, så det også kan åbnes fra tastaturet; en kontrol, kun en mus kan nå, er ikke en kontrol alle har. To ting rettet undervejs, som først kunne ses ved at kigge: billedet fik lov at blive højere end skærmen, så billedtekst og lukkeknap røg op over kanten — og lukkeknappen er den synlige vej ud. Og dialogen fyldte hele bredden i stedet for at slutte om billedet, så skærmbilledet stod op ad venstre kant med knappen ude i det modsatte hjørne. Nu er den fit-content og centreret, og alt er inden for vinduet ned til en telefonbredde. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
134fe8918e |
chore: hold overleveringen uden for repoet
Den navngiver panelet, server-id'et og produktionsadressen — et kort over én persons opsætning, og det hører ikke til i et offentligt repo. Ingen adgangskoder, men et server-id og en panel-adresse er præcis det, et lækket token skal bruge for at være farligt. Filen er fjernet fra hele historikken, ikke kun fra spidsen: at slette den i en ny commit ville have efterladt den læselig i alle de gamle. Den ligger stadig på disken, for det er sådan en session afleverer til den næste. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
3fd16f71df |
fix(reminders): en påmindelse, ingen modtager, er ikke en påmindelse
Påmindelser blev sendt i appen og på mail. Har man fanen lukket og ingen mailserver sat op — hvilket er præcis det, docker-linjen i dokumentationen giver — kom der ingenting, mens indstillingssiden sagde at notifikationer var slået til for enheden. Push-halvdelen var aldrig hægtet på. Notifikationer fra andres handlinger gik ud; kun de tidsstyrede gjorde ikke, og det er dem, man har slået til for at slippe for at holde øje selv. Jobbet har nu en vej til at sende, leveret af HTTP-laget, som ejer VAPID-nøglerne og listen over enheder — samme mønster som synkroniseringerne. Prøven ser efter, at den når frem, og at den kun når frem én gang: en påmindelse, der ankommer ved hver fejning, er værre end en, der ikke ankommer. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
37605ba684 |
feat(site): landingssiden siger hvad der findes, og vejer en femtedel
Skærmbillederne lå som PNG i fuld skærmopløsning — 2,1 MB for syv billeder på en side, der viser dem under 900 pixels bred. Som WebP i dobbelt visningsbredde fylder de 424 kB. Samme billeder, samme skarphed, en femtedel af vægten. Indholdet fangede aldrig op på det, der blev bygget: postkasser over IMAP ved siden af Gmail, passkeys, at man kan skrive hvor som helst for at fange noget, at hemmeligheder ligger krypteret med nøglen uden for databasen, og historikken. Ni felter nu, hvilket også fylder gitteret ud i stedet for at efterlade et tomt. Og en fejl, jeg selv lavede undervejs: jeg lagde en description-meta ind, fordi min søgning ikke så den, der lå i forvejen — den var delt over flere linjer. Dubletten er væk, og den oprindelige er opdateret i stedet. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
6e941f9928 |
fix(mailboxes): to kørsler af samme postkasse må ikke læse den samme markør
Fyrre kopier af én mail ankom i samme sekund. Én kørsel kan højst lave femogtyve, så der var to i gang: et tryk på "Hent nu" mens ti-minutters-fejningen allerede var inde i den samme postkasse. Begge læste markøren som den stod, begge hentede de samme beskeder, begge lavede opgaver ud af dem. Markør-rettelsen fra i går var altså rigtig, men ikke nok — den virker, når der kun er én læser. Rækken læses nu igen under låsen, og låsen holdes hele vejen igennem: slap man den, før markøren var skrevet, ville hullet stå åbent præcis der, hvor det skulle lukkes. Et kort pr. postkasse frem for én lås. To menneskers postkasser har intet at sige hinanden, og én fælles lås ville gøre én langsom vært til alles problem. Prøven kører to kørsler oven i hinanden med vilje og tæller, hvor mange der er inde ad gangen — en lås er kun værd at have, hvis noget prøver døren. Den holder også øje med, at to forskellige postkasser ikke venter på hinanden. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
993822f6fd |
fix(panel): bed om en planlagt genstart, ikke en der dør med afsenderen
Release / Build and publish image (push) Failing after 10m6s
Knappen tog produktionen ned og lod den ligge. Panelets almindelige restart udfører arbejdet mens forespørgslen står åben: den stopper containeren — hvilket slår denne proces ihjel — og panelet, der ser afsenderen forsvinde, når aldrig til den halvdel, der starter igen. Fire minutters nedetid den 18. august, og det kan ikke lappes herfra: enhver knap, der synkront beder om sin egen død, har samme form. safe-restart planlægges i stedet. Panelet svarer "Restart scheduled" og gør arbejdet bagefter for egen regning, så intet afhænger af, at denne proces stadig er i live til at høre svaret. Prøven hang bagefter, og det var prøvens skyld: dens falske panel læste aldrig forespørgslens krop, og der er en nu. En server, der ikke læser hvad den fik, opdager aldrig at afsenderen har lagt på — så httptest ventede på en handler, der aldrig vendte tilbage, og hele binæren løb tør for tid i stedet for den ene forespørgsel. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
b0f78d1549 |
feat(gmail): Gmail flytter ind i postkasse-tabellen
Ét sted at spørge "hvilke postkasser har den her person". Gmails tokens, udløser, etiket, set-liste og målprojekt ligger nu som en række i mailboxes ved siden af dem, der læses over IMAP, og baggrundsjobbet fejer én gang i stedet for to og skifter vej efter rækkens kind. PKCE-værdierne bliver i user_settings. De hører til ét forsøg på at logge ind, ikke til en postkasse, og de skal væk igen når forsøget er ovre. Målprojektet fik sin egen kolonne, som IMAP-postkasser også får glæde af — en arbejdspostkasse, der filer i arbejdsprojektet, er det åbenlyse at ville have. Ikke en fremmednøgle: sletter man projektet, skal destinationen stille holde op med at gælde, ikke afvise sletningen eller tage postkassen med sig. Flytningen kunne først køre nu. Den tømmer indstillingsrækken, og havde den ligget i v0.14.0, mens handlerne stadig læste den, var en fungerende postkasse blevet koblet fra. Værdierne kopieres som chiffertekst — begge tabeller forsegler med samme nøgle — så migrationen behøver ikke nøglen, hvilket en migration heller ikke har noget at gøre med at holde. Der er en prøve for præcis dét: en Gmail forbundet før flytningen, med et forseglet token skrevet som indstillingstabellen skrev dem, kommer ud på den anden side med token, udløser, adresse, målprojekt og set-liste i behold. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
f3fecbd0b4 |
fix(imap): samme mail blev en ny opgave hver gang, og uddraget var rå MIME
To fejl fra ét skærmbillede. Markøren stod stille. Den blev sat ud fra uid'en i fetch-svaret, og den kommer ikke fra alle servere — så next-run læste fra nul hver gang. Den sættes nu ud fra hvad SØGNINGEN fandt, når kørslen kom hele vejen igennem: så er alt op til det punkt betragtet, uanset hvad fetch-svaret bar med. En afbrudt kørsel flytter kun til den sidste, den faktisk lavede en opgave af. Prøven mod hukommelsesserveren ville aldrig have fanget det, for den svarer altid med uid. Og uddraget hentede BODY[TEXT], som ved multipart er grænser og dele-hoveder og det hele — derfor stod der "--=_be71cb39… Content-Transfer-Encoding: 8bit" foran teksten i hver eneste opgave. Nu hentes hele beskeden og MIME-strukturen gås igennem: text/plain foretrækkes, HTML er reserven med mærkerne pillet af. Groft, for det er linjen under en overskrift og ikke en gengivelsesmotor. Multipart-prøven er sat på prøve: med den gamle hentning fejler den med præcis den tekst, skærmbilledet viste. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
30adc69cb5 |
feat(mailboxes): plads til Gmails udløser og dens set-liste
Kolonnerne, ikke flytningen. Gmail-handlerne læser stadig user_settings, og en migration, der tømte den tabel før de var skrevet om, ville koble en fungerende postkasse fra. Det additive først; flytningen når koden på den anden side er klar til den, og den ligger som PENDING_gmail_move.sql.txt uden for .sql-mønstret, så den ikke kan køre ved et uheld. Udløseren fik sin egen kolonne frem for at blive pakket ind i label med et kolon. En kolonne, der rummer to ting adskilt af et tegn, er en kolonne, nogen læser forkert senere — og en etiket med kolon i navnet ville gøre det på første dag. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
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 <[email protected]> |