fcedab0b19f2c865279d85f5cc408e04e247c0f2
228
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fcedab0b19 |
docs(site): add the new features and light screenshots of them
The landing page did not mention two things the app grew — sharing a note with a person, and task durations — and its screenshots were all in the dark theme. - Copy: a cell for sharing a note or a page (to a person, or by filing it in a project, with the Shared with me group), and one for how long a task takes (drawn to length on the calendar, dragged by its foot, kept when moved). - Two new screenshots in the light theme, at the top of the gallery: the calendar drawing timed tasks to their real length, and a note somebody shared gathered under Shared with me. English throughout, like the rest of the site — the capture account is English, so the interface is. The generator grew the demo data these need: a handful of timed tasks with lengths across the week, and a note the colleague shares back to the owner. Every other screenshot was recaptured from the current build in the same pass, so the whole set reflects what ships today (the tidied note footer, and the rest). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
72b6ea3803 |
Merge pull request #19 from kristianwind/task-durations-calendar
Task durations: draw, resize, and edit a task's length |
||
|
|
4f25905d81 |
feat(calendar): a task's length is drawn, resizable, and editable
A timed task was drawn at a fixed half hour whatever its `duration_min` said, so a length the data model already carried was invisible and uneditable. Three parts: - The calendar draws a task at its real length — a quarter-hour floor so a zero-minute task is still grabbable, the day's end as the ceiling — instead of a flat thirty minutes. A ninety-minute task now looks like ninety minutes. - A grip along the foot of a timed task drags its length. Pointer events, not the HTML5 drag the body uses, and stopPropagation between them — otherwise making a task longer would pick it up and move it instead. It snaps to the quarter hour, follows the handle live, and writes once on release through a new store `resize`. Moving a task (the existing day/time drag) leaves the length alone. - The task drawer gains a length field beside the time — minutes, shown only with a time to hang it on, since a duration without a start is a length of nothing and the calendar has nowhere to draw it. So a length can be set off the calendar too. e2e: dragging the foot of a task makes it longer both on screen and in what the server stores, and the time does not move. Full Chromium suite green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e3eeb3037c |
Merge pull request #18 from kristianwind/tidy-note-share-footer
Tidy the note sharing UI: gather it behind a Del button |
||
|
|
97e6be3afa |
fix(notes): gather sharing behind a Del button so the footer stays tidy
The sharing controls were laid out across the note's footer — the project select, the whole "Del med personer" panel and the Save/Delete buttons all on one row. It crammed: "Læg i projekt" wrapped to two lines and the people panel ran into the rest. Now one Del button in the footer opens a popover that holds both ways a note is shared — a project and named people — stacked in a small card. The footer is a status on the left and Del / Gem / Slet on the right, and nothing wraps. The button carries a count when the note is shared with someone, so "shared" is legible without opening it. The popover closes on a click outside, on Escape, and on switching to another note — the last tracked by note id, so a save returning a fresh copy of the same note does not slam it shut mid-use. The inner "add someone" button is now Tilføj, so it does not read as a second Del. e2e updated to open the popover before reaching the controls; full Chromium suite green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d67b61d3c6 |
Merge pull request #17 from kristianwind/fix-mono-block-escape
Fix: let a monospace line be left — on return and through the menuv0.41.1 |
||
|
|
f0d18bb800 |
fix(notes): let a monospace line be left — on return and through the menu
A <pre> is preformatted, so the browser answered Enter by inserting a <br> and
keeping the caret inside it. A monospace last line therefore trapped everything
typed after it in monospace, with no way back to body by typing — and the style
menu could not get out either: `formatBlock('p')` on a line inside a <pre> nests a
<p> *inside* the block (`<pre>…<p>body</p></pre>`), so the text stayed monospace
and the note was left malformed. Reported as "monospace keeps going and I can
neither delete it nor switch to body".
Two fixes, both in the editor:
- Plain Enter in a <pre> now steps out: the tail after the caret becomes a
paragraph below the block, and that is where the caret lands. Shift+Enter is
untouched, so a soft line break still writes multi-line code inside the block —
Enter to leave, Shift+Enter to stay.
- The style menu converts anything touching a <pre> by hand, replacing the element
rather than reformatting inside it, so a monospace line goes cleanly back to body
(or to a heading) instead of nesting.
The split can leave an empty text node in the new paragraph, and a block with no
element and no text is not a caret host in Chromium — the next keystroke falls back
into the block above. So an empty paragraph is given a <br> to stand in, which was
the difference between the caret landing in the new line and the text reappearing
in the code.
e2e: a monospace line left on Enter is body after it, Shift+Enter stays monospace,
the menu returns it to body, and the whole thing survives the round-trip through
Markdown and a reload.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
7f8d925299 |
Merge pull request #15 from kristianwind/share-notes-between-users
Share notes with people, not only through a projectv0.41.0 |
||
|
|
cdab817141 | Merge main: pick up the calendar e2e flake fix | ||
|
|
2e76ced5fa |
Merge pull request #16 from kristianwind/fix-flaky-calendar-drag
Fix flaky calendar e2e: count the test's own events, not the whole day |
||
|
|
711fb4838b |
test(e2e): the calendar test counted other tests' tasks as its own
The calendar test asserted `[data-date=day] .tevent` had a count of exactly one — the mocked meeting — after dragging its task to the next day. That count assumed the day held nothing but this test's own events. It does not: the suite shares one database, and an earlier test drags a task across a month boundary, which on some dates lands a timed task on this very day. A timed task renders as a `.tevent` too, so the count was two, and the test failed — but only in the full run, and only when the date arithmetic put the two tests on the same cell. Alone it passed, which is exactly the shape of a shared-state collision rather than a real bug. Named, not counted: assert the meeting is still there and the task is no longer in the day's column, which is what "the events stayed, the task moved" actually means — and is true however full another test left the day. Same for the all-day band. No product code changed; the drag and everything it checks are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
643b30555e |
test(e2e): follow the renamed share label, and cover person-to-person sharing
The project-filing control was relabelled "Del i" → "Læg i projekt" and its empty option "Kun mig" → "Intet projekt" (a note can now be shared with people while under no project, so "kun mig" was no longer true). The two e2e tests that drove that control by its old label are updated to the new one. And a new test for the feature itself: an owner shares a note with a second account from the Del med personer panel, the recipient's own list grows a Delt med mig group with the note in it, a viewer's row carries no star or archive control, and unsharing takes it back off their list. The recipient is navigated on the origin the invite signed her in on — VERDANDE_BASE_URL is localhost while the suite drives 127.0.0.1, and a session cookie set on one host is not sent to the other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
91671a4f97 |
feat(notes): share a note with a person, not only through a project
Until now a note was shared exactly one way: file it in a shared project, and
everyone who could read the project could read the note. That is right when the
note belongs with a body of work and wrong when it is just "let Sofie see this" —
it made you stand up a project to hand over a single page.
A note can now be shared with named people directly, at viewer or editor — the
same two roles a project membership uses, so there is one idea of a role in the
program and not two. The paths stack: a note can be loose, filed, shared with
people, or any mix, and access is the most any of them allow.
The store:
- note_shares (migration 0030): note ↔ user ↔ role, one row per person, owner
recorded. Visibility (AllNotes, and search via mayTouchNote) widens to include
notes shared directly with the reader.
- the effective role folds a direct share on top of the owner and project paths.
The rules, drawn where they protect the owner:
- read = viewer, edit text = editor, on any path.
- MOVE a note (change its project) is owner-only: an editor filing it into a
project the owner is not in would take the owner's own note away from them.
- DELETE is owner or project-editor — a direct share, however high its role,
does not reach to throwing the note away.
- SHARE and UNSHARE are the owner's alone, like a project's invitations.
- the picker is everyone on the instance (a closed, invited set) minus who is
already on the note; a made-up id and the owner themselves are refused.
The list:
- a "Delt med mig" group at the top, collapsible, that appears only when it has
something in it — the same rule Favoritter follows — so it costs nothing when
you share nothing. A shared note lives there, not also in a time bucket.
- every shared note carries a small chip in its owner's avatar colour, the
project mark's shape said for a person.
- no star or archive button on a note somebody else owns: those change the note
itself, and it is not yours to put away.
- the editor grows a "Del med personer" panel for the owner: who can see it, at
what role, and an add row drawn from the instance's other people.
The old comment in the notes footer argued against a note having an access list
of its own. That was the right call until the case it did not cover — sharing
without a project — turned out to be the one actually asked for. Updated to match.
Person now carries json tags, so a note's owner serialises as id/name/avatar_color
like every other person in the API rather than as Go field names.
Tests: a shared note reaches the other person marked and owned; a viewer reads but
cannot write; an editor writes but cannot re-share or delete; unshare removes
access and is owner-only; the candidate list excludes who is already on and the
owner; and a made-up id and self are refused. OpenAPI covers the three new routes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
15be5d2165 |
Merge pull request #14 from kristianwind/security-hardening-headers
Security hardening: HSTS, forwarded-IP trust, single-use TOTP |
||
|
|
77994eff09 |
feat(security): a TOTP code can be used once
RFC 6238 says the verifier must reject a code it has already accepted: a code stays valid for its whole thirty-second window, so one seen in flight — a phishing page, a proxy, over the shoulder — could be replayed until it expired. Tasks now record the counter of the last accepted step (totp_last_step), and a login accepts a code only when its step is strictly greater. The guard is in the UPDATE, not a read-then-write, so two logins racing with the same stolen code cannot both find the old value and both proceed — exactly one moves the row. auth.TOTPStep is VerifyTOTP that also reports which step matched, walking the same one-step window; the code entered a little early or late spends the step it was issued at, not the step it was checked at, so a late-but-valid code is still single-use. Verified end to end: the same code presented to two logins in one window is accepted once and refused the second time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b4038356f0 |
feat(security): trust a forwarded IP only from a proxy, and set COOP
The rate limiter and the audit log key on the caller's address, and that address was read with chi's middleware.RealIP — which believes X-Forwarded-For from anyone. So a caller could set it to a new value on every request and get a fresh password-guess bucket each time, and could write any address they liked into the log the panel's watchers read. The one control meant for when a password is already gone was keyed on a number the attacker got to pick. Now the header is believed only when the machine that *connected* is a trusted proxy — by default anything on a private network, which is the shape of a reverse proxy or tunnel sharing the host or the docker bridge. A connection straight off the internet is trusted for nothing and is keyed on the address it actually came from. VERDANDE_TRUSTED_PROXIES overrides the set (CIDRs, or "none" to trust no proxy at all); VERDANDE_REAL_IP_HEADER names the header. Verified in the current deployment before the change: behind the Cloudflare Tunnel a spoofed X-Real-IP already got no fresh bucket, because cloudflared overwrites it. This makes that safety a property of the code, not of the deployment — expose the instance directly, or put a header-passing proxy in front, and it would have been a live hole. Also COOP: Cross-Origin-Opener-Policy: same-origin on every response, isolating the browsing context. verdande scripts no other window, so there is nothing to lose and a class of cross-window attacks to close. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8a87cfeb44 |
feat(security): HSTS og en lukket Permissions-Policy på hvert svar
To headere, appen ikke satte, og som begge følger af, hvad den allerede er. Sessionen bor i en __Host-cookie, der kun findes over HTTPS — hele modellen forudsætter TLS. Uden HSTS holder den forudsætning ikke mod en netværksangriber: den allerførste navigation kan strippes til http://, og cookien rækkes over i klartekst, før Secure-flaget nogensinde kommer i spil. HSTS lukker vinduet, og den er sat på nøjagtig samme betingelse som cookiens Secure-flag (secureCookies), så en dev-instans på ren HTTP aldrig bliver bedt om et certifikat, den ikke har. To år, underdomæner med; ingen preload — at blive hard-kodet ind i browsere er en beslutning for domænets ejer, ikke for en selv-hostet app. verdande beder aldrig om kamera, mikrofon eller placering, så den sikreste Permissions-Policy er at slå dem alle fra: et indsat element kan ikke bede om det, siden selv har afvist. Navngivet felt for felt frem for med et jokertegn, fordi jokertegns-formen ikke forstås ens af alle browsere, og en regel, en browser ignorerer, er ingen regel. Prøve på begge: HSTS følger HTTPS og udebliver over HTTP, og Permissions-Policy lukker for kameraet uanset skema. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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.v0.40.3 |
||
|
|
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.v0.40.2 |
||
|
|
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 <noreply@anthropic.com> |
||
|
|
8d5dd1f1c3 |
fix(notes): grupperne stod tomme — hver liste rullede for sig
Release / Build and publish image (push) Failing after 9m59s
Meldt ind som "noter vises ikke i den nye listeform": hver eneste overskrift stod der med sit tal, og der var ikke en eneste note under nogen af dem. Før var der ét `<ul>`, og *det* rullede. Da listen blev delt i grupper, blev hver gruppe sit eget `<ul>` — og dermed sit eget flex-element med `overflow-y: auto`. Flex-elementer skrumper som udgangspunkt, og et element, der skrumper og klipper, viser ingenting. Med tyve måneder i søjlen fik hver liste nul at være høj i. Rulningen hører til kassen om dem alle, ikke til hver af dem. **Prøven fangede det ikke, og det er værd at forstå hvorfor.** Den lavede to noter, altså tre grupper, i et vindue på 720 pixels — og med så få grupper er der plads nok til, at skrumpningen ikke bider. Fejlen kræver pres. Prøven gør vinduet lavt nu og kigger efter, at rækkerne kan *ses* og ikke bare findes, og den siger desuden hvor rulningen ligger: på kassen, ikke på listerne. Den er set fejle med den gamle stil. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>v0.40.1 |
||
|
|
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 <noreply@anthropic.com>v0.40.0 |
||
|
|
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 <noreply@anthropic.com> |
||
|
|
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 <noreply@anthropic.com>v0.39.1 |
||
|
|
2219f6af13 |
fix(i18n): en gentagelse siges nu på læserens sprog
Release / Build and publish image (push) Failing after 10m2s
`recurrence.Describe` skriver dansk. Fladen kunne navngive de fem almindelige regler selv, og alt andet — "hver mandag", "hverdage", "hver anden uge tirsdag" — faldt tilbage til serverens tekst, som er dansk uanset hvad kontoen er sat til. Serveren kan ikke bare tage en locale med, og det er ikke dovenskab: `toTaskJSON` har niogtyve kaldesteder, og mange af dem er WebSocket-udsendelser, hvor én nyttelast når alle i et projekt på én gang. Der findes ikke ét sprog at skrive den i. Men reglen er den samme, uanset hvem der læser den. Den ligger allerede i nyttelasten, så beskrivelsen hører hjemme dér, hvor læseren er — i fladen. Serverens tekst bliver stående som reserve. Den er stadig den, et API-kald og en MCP-forbindelse får, og de har ingen flade at oversætte i. Ugedagen kommer fra `Intl` og ikke fra en tabel — ikke for at spare syv strenge pr. sprog, men for at få de navne, sproget faktisk bruger. Dagene sorteres, som ugen går, frem for som reglen tilfældigvis skrev dem: "mandag og torsdag" og "torsdag og mandag" er den samme regel, og den ene af dem er den, folk siger. Og to har sit eget ord på begge sprog — "hver anden", "every other". Det er ikke tallet to sat ind i den samme sætning, så det er sin egen nøgle. Et sprog, der bøjer enheden efter tallet, kan ikke sættes sammen af stumper; derfor står tallet inde i teksten frem for at blive limet på. Prøven bruger `hver mandag` og `hverdage`, fordi de netop *ikke* står på listen over de fem, fladen altid har kunnet. En tredje vending, jeg først skrev prøven med — "hver anden tirsdag" — viste sig ikke at være en, hurtig tilføjelse overhovedet læser: den lagde "hver anden" i titlen og gjorde "tirsdag" til en dato. Det er en anden sag, og den står nu skrevet ét sted, hvor den kan tages op. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>v0.39.0 |
||
|
|
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 <noreply@anthropic.com> |
||
|
|
868588de10 |
feat(projects): et projekts kalender kan vises som en uge
Ugen fandtes på Kommende og på Kalender, men ikke dér hvor et projekt bor — og det
er dér, "hvornår sker de her fire ting" oftest bliver spurgt.
Inde i kalendervisningen frem for som en fjerde `view_mode`. `view_mode` er
projektets og delt med alle, der kan se det, mens uge eller måned er et spørgsmål
om hvor bred skærmen er, foran hvilken der sidder én person — samme grund som
sidebjælkens bredde ligger i localStorage af. Og en fjerde værdi ville kræve en
tabelombygning: `CHECK (view_mode IN ('list','board','calendar'))` kan SQLite ikke
ændre på plads.
**Og så fandt den en fejl, der har ligget og ventet.** `cursor` blev sat med
`span === 'week' ? … : …`, hvilket læser `span` i det øjeblik komponenten bliver
til og fanger den værdi for altid. Byggeriet har advaret om det lige så længe. Så
længe hver visning havde sin egen komponent, gjorde det ingen forskel — og det
gjorde det i samme sekund, en vælger begyndte at skifte `span` på den samme.
Markøren stod på den 1. i måneden, og et skift til uge landede på ugen omkring den
1.: "27. jul.–2. aug." for en person, der kiggede på august.
Rettet, med den ene undtagelse skrevet ned: en månedsvisnings markør *er* den
første i måneden, så et skift til uge, mens man står i den indeværende måned,
skal lande på den uge, i dag ligger i — ikke på ugen omkring den 1.
Og en anden fælde, jeg selv gik i undervejs: en underklasses felter sættes *efter*
basens konstruktør er kørt. Nøglen til localStorage som et felt på en underklasse
betød, at valget blev gemt ét sted og læst et andet — en uge, man havde valgt, var
en måned igen efter en genindlæsning. Nøglen er et argument nu; et argument har
ingen rækkefølge at tage fejl af.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
dcb71f4c14 |
feat(calendar): abonnement på flere kalendere, ICS og webcal
Release / Build and publish image (push) Failing after 10m3s
Bedt om som "abonnering af flere kalendere (ics og webcal?)". Det er også den eneste vej for en privat konto: instansens OAuth-klient er Internal, og Google afviser en konto udefra med `org_internal`, uanset hvad der ellers er sat op. **Samme tabeller.** Et abonnement er én kalender med begivenheder i, og det er præcis det, `calendars` og `calendar_events` allerede beskriver — med vinduet, farven, `shown` og hele fejemaskinen omkring sig. En tabel til ville være en anden vej til det samme sted, og to veje til ét sted er to steder at rette den dag noget om begivenheder ændrer sig. Det, der skulle laves om, var identiteten. En OAuth-konto er én pr. person og udbyder — tokenet er identiteten — mens et abonnement er én pr. adresse, og man har flere. `calendar_accounts_once` er derfor delvist nu: det holder for rækkerne uden adresse, og et andet indeks holder adresserne unikke. Delvise indekser er den ene ting, SQLite kan her, som ellers krævede en tabelombygning. **`webcal://` skrives om til `https://`.** Det er ikke en protokol, noget taler — det er en konvention, der får et skrivebord til at give adressen til kalenderprogrammet i stedet for til browseren, og alle udgivere skriver deres links sådan. At afvise den ville være at afvise folk på en teknikalitet. **Adressen gennem `safedial`, begge halvdele.** Afvist hvor den skrives ned, så den, der står med feltet, får svaret med det samme — og igen på den *opslåede* adresse hver gang den hentes, fordi et navn, der blev godkendt ved indtastningen, kan svare 127.0.0.1, når fejemaskinen kører klokken tre om natten. Det er den sætning, `internal/safedial` selv står og advarer om, og den blev skrevet efter et SSRF-fund i morges. **Hentet én gang, før den gemmes.** En adresse, der ikke kan læses, er en tastefejl langt oftere end en server, der tilfældigvis er nede, og at sige det, mens nogen står med feltet, er de to sekunder værd. Et abonnement, der lydløst aldrig fyldes, er den samme tavshed, notifikationerne havde. Læseren er ny i `internal/ics`, som hidtil kun kunne skrive. To ting, den med vilje ikke gør, står skrevet ved den: den udfolder ikke gentagelser, og den opløser ikke tidszoner. Begge dele er større end de ser ud, og begge har ét sted at høre til den dag de skal. En ting, formatet lærte: DTEND er eksklusiv for en heldagsbegivenhed. Gemmes den, som filen skriver den, dækker hver eneste helligdag også dagen efter sig. Én adresse, der fejler, stopper ikke de andre. Et abonnement er en anden persons server, og den ordinære tilstand for en anden persons server er, at den er i orden indtil en morgen hvor den ikke er. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>v0.38.0 |
||
|
|
968a29ca67 |
feat(notes): listen står i grupper, og projektet har sin egen farve
Release / Build and publish image (push) Failing after 9m56s
Meldt ind som "den er for tæt på en eller anden måde". Grunden var ikke afstanden mellem rækkerne: hver række sagde tre ting, og to af dem gentog sig selv nedad. Datoen stod på hver eneste række — også når tyve noter deler dag — og projektet fik en linje for sig selv. Tre linjer pr. note, gange tolv hundrede. Gentagelserne er flyttet derhen, hvor de siger noget. **Datoen er blevet til overskriften.** I dag · I går · Denne uge · August 2026 … og så er den væk fra rækken. Noten går fra tre linjer til to, og luften kommer af, at der er mindre at skrive — ikke af at listen bliver længere. **Favoritterne har fået deres egen gruppe.** De lå allerede først; der stod bare ikke hvorfor. **Grupperingen følger sorteringen.** Sorteret på "Oprettet" grupperes der på oprettelsesdatoen, på "Senest rørt" på den — ellers ville listen stå i én rækkefølge og være grupperet efter en anden, og så er overskrifterne løgn. Sorteret på navn er der ingen dato at gruppere på, så det bliver forbogstavet, og datoen kommer tilbage i rækken, hvor den så er det eneste tilbage, der siger noget. **Arkivet grupperes med.** Det er dér, de tolv hundrede importerede ligger, og dér, det betyder mest. **Projektet har sin egen farve** — den samme prik som i sidebjælken. Mærkaten sagde det samme som alle de andre i en grå, der var lysere end uddraget, og projektet er dét, man skanner efter, når man leder i tolv hundrede noter. Prikken bærer farven og ikke teksten: fem projektfarver som skriftfarve er fem forskellige grader af læsbarhed, og den lyseste er ikke læsbar. Fire ting, der blev fundet ved at se på det: - Uddraget gentog titlen, fordi titlen *er* første linje. Det var skjult, så længe en dato stod imellem dem. - Uddraget klippes ikke af sig selv. Det lå før i en flex-boks, der ejede afkortningen; da datoen flyttede op, forsvandt boksen, og sætningen løb videre ind under favoritstjernen. - Overskriftens stjerne hed `.star` ligesom rækkens favoritknap og arvede dens gennemsigtighed. Den stod der aldrig. To ting, der ligner hinanden, er ikke det samme. - En `aria-label` på foldeknappen erstatter knappens indhold som dens navn — og knappen *er* overskriften, så overskriften kom til at hedde "Fold I dag sammen". Sidebjælkens gruppehoveder gør det rigtigt, og gør det nu begge steder. Sammenfoldning huskes ikke på tværs af besøg: en gruppe, der åbner foldet uden at nogen bad om det, er en liste, der ser tom ud. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>v0.37.0 |
||
|
|
fcd6dca852 |
fix(push): "der kommer ikke nogen" kunne ikke besvares
Release / Build and publish image (push) Failing after 10m0s
Alt om det var usynligt. Indstillingssiden sagde "til for denne enhed", så snart der fandtes et abonnement — også når hver eneste push blev afvist. Og en afvist push gik i en logline på **Debug**, som ikke skrives på en instans, der kører på standardniveau. Så en levering, der fejler lydløst, og en levering, der aldrig sker, så ens ud, og forskellen er hele diagnosen. Tre forskellige problemer havde ét udseende: browseren tilmeldte sig aldrig, tjenesten afviste den, eller der var ikke noget forfaldent endnu. **En prøveknap** under Indstillinger → Notifikationer sender én notifikation gennem den rigtige rute — ikke en genvej, så det, den beviser, er det, der faktisk skal virke — og siger hvad hver enhed gjorde ved den. Kom den ikke frem, og afviste ingen den, står der, at det så er browseren eller styresystemet, der holder den tilbage. Nu peger svaret på ét af de tre steder. Loglinjen er hævet fra Debug til Warn. Det var den ene linje, der ville have sagt hvorfor. Teksten kommer fra klienten, fordi ordbøgerne gør. En server, der bærer sin egen kopi af "det her er en prøve", bærer den på ét sprog. **Og en lækage, prøven fandt.** Go pakker en transportfejl som `*url.Error`, og dens tekst er `Post "https://fcm.googleapis.com/fcm/send/dQw4…": dial tcp: …` — hele endepunktet, som er den legitimation, push-tjenesten udstedte til den enhed. Den var på vej ud i svaret, i loglinjen og i ethvert skærmbillede af nogen af dem. Prøven var skrevet for at bevise, at kun værten blev nævnt, og det var sådan den blev fundet. Fejlen pakkes ud, og adressen skrubbes bagefter alligevel: den næste fejltype, der bærer den med, kommer ikke med en advarsel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>v0.36.1 |
||
|
|
ae673086bc |
feat(tasks): tid på en opgave, og et træk i kalenderen sætter den
Release / Build and publish image (push) Failing after 9m58s
Bedt om: "jeg vil have tid på opgaver, og jeg vil kunne justere dem i kalenderen."
Serveren kunne det hele i forvejen — `due_date` og `due_time` læses sammen og
bliver til et `due_datetime` i kontoens tidszone. Det var fladen, der ikke havde
nogen vej til at sige det.
**Et felt i opgaveruden.** Tomt, indtil nogen sætter det: en opgave uden
klokkeslæt er det almindelige, og et felt, der åbner på 00:00, påstår, at noget
skal ske ved midnat.
**Og en fejl, der lå og ventede.** Serveren læser en dato uden en tid som "ryd
tiden". Så et gem af datoen alene tog klokkeslættet med sig — man rettede dagen og
mistede timen — og det samme gjorde hvert eneste træk i kalenderen. Ingen af
delene sagde noget.
`reschedule` siger nu tiden højt, tre måder, fordi de to første ligner hinanden i
JavaScript og derfor skal skilles ad ét sted frem for at blive forvekslet fire:
undefined behold den, opgaven har — månedens ruder, hvor der ingen tid er at læse
'' ryd den — heldagsbåndet
'HH:MM' sæt den — et sted på døgnet
**Hvor på søjlen man slipper, er klokkeslættet.** Målt mod søjlens egen kasse frem
for mod siden, så det bliver rigtigt uanset hvor gitteret sidder og hvor langt der
er rullet. Rundet til kvarter: minuttet under markøren er ikke en oplysning, nogen
har — pegefingeren er bredere end et minut — og 14:07 er et tal, der ser ud som om
det blev valgt.
Prøven går hele vejen: sæt tiden, genindlæs og se at serveren tog den, ret datoen
og se at timen bliver, find opgaven på sit klokkeslæt i ugegitteret, og træk den op
i båndet igen. To ting, den lærte undervejs, står skrevet ved den: et træk over
fire hundrede pixels skal gå i skridt, ellers har browseren for få mellemstationer
til at holde gesten i live — og klokken skal være tidlig, for klokken fjorten
ligger under skærmkanten, og at måle noget dernede ruller siden væk under målet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
v0.36.0
|
||
|
|
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 <noreply@anthropic.com> |
||
|
|
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 <noreply@anthropic.com>v0.35.1 |
||
|
|
9d34b57166 |
feat(calendar): ugen er et døgn, og dagene er lige brede
Release / Build and publish image (push) Failing after 9m58s
To ting, meldt ind fra en uge, der ikke svarede på det, man åbnede den for. **Dagene var ikke lige brede.** Gitteret stod på `repeat(7, 1fr)`, og `1fr` er `minmax(auto, 1fr)`: `auto` som minimum betyder, at en søjle ikke kan blive smallere end sit længste ord. Én opgave, der hed "Gårdbutikken på Møllerup Gods — opfølgning på mail", gjorde onsdag dobbelt så bred som mandag og klemte resten af ugen sammen om den. `minmax(0, 1fr)` lader søjlen blive så smal, den skal, og så klipper brikken sin tekst af — hvilket den allerede kunne. **Ugen var syv punktopstillinger.** Den svarede på "hvad skal der ske torsdag" og ikke på "hvornår", og det andet er det, man åbner en uge for. To møder klokken ti er en konflikt, man skal kunne se, ikke læse sig til. Nu et gitter med en tidsrende og syv dage: timerne ned ad siden, begivenheder placeret efter klokkeslæt, og et bånd foroven til det, der ikke har et — en heldagsbegivenhed lagt klokken nul ville påstå, den holdt fra midnat til midnat. Overlappende ting deler bredden i sammenhængende klynger frem for parvist: overlapper A med B og B med C, skal alle tre dele, også når A og C ikke rører hinanden. Ellers ville A og C hver tro, de havde halvdelen, og lægge sig oven på hver sin halvdel af B. Hvilke timer der tegnes, kommer fra ugens egne begivenheder med arbejdsdagen som gulv. Et helt døgn er fireogtyve rækker, hvoraf de otte altid er tomme, og så er gitteret noget man ruller i frem for noget man ser — men en vagt klokken fem skal med, uden at alle andre uger betaler for den med tomme rækker. Måneden er ikke rørt ud over søjlebredden. En dag i en månedsrude er halvanden centimeter høj; et døgn tegnet i den er ikke en visning, det er en streg. Brikkerne er flyttet ud i to snippets, som begge visninger tegner med. De viser de samme to ting og skal vise dem ens; det er placeringen, der er forskellig, og så er det kun placeringen, der står to steder. `data-date` følger med over på ugens timesøjler — det er sådan noget uden for komponenten peger på en dag. Båndet får sit eget `data-allday`, så de to ikke bliver til det samme svar på ét spørgsmål. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>v0.35.0 |
||
|
|
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 <noreply@anthropic.com> |
||
|
|
b87c9ea6e2 |
fix(auth): en afvist passkey sagde "forkert e-mail eller adgangskode"
Release / Build and publish image (push) Failing after 10m0s
Der er hverken en e-mail eller en adgangskode i et passkey-login. Loginet er discoverable — enheden ved selv, hvilken konto nøglen hører til, og der bliver ikke tastet noget. Alligevel fik en afvisning fladens generiske 401-tekst, og den nævner begge dele. Det sendte en person ud at lede efter en tastefejl i sin konto, der aldrig havde været der. Det, der faktisk var sket, var at instansen var flyttet fra http til https: en passkey hører til præcis den adresse, den blev lavet på — `RPOrigins` bygges af `VERDANDE_BASE_URL` med skema og alt — så et login på det ene skema mod en server, der venter det andet, kan ikke verificeres. Beskeden siger nu det, og siger også hvor man skal kigge. Serveren har hele tiden svaret "that key was not accepted"; det var oversættelsen i fladen, der gjorde den til noget andet. Aflysning bliver behandlet som før — den er ikke en fejl, og en fejl for den ville læse som om nøglen blev afvist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>v0.34.3 |
||
|
|
ce1d27f8ca |
fix(notes): én note, der ikke kunne tegnes, låste hele ruden
Release / Build and publish image (push) Failing after 9m53s
Meldt ind som "jeg kan ikke skifte mellem noter — uanset hvilken jeg trykker på, åbner de ikke". Editoren indlæste med `note.body.trim()` uden værn. `body` er en streng fra serveren hver eneste gang — indtil den ikke er, og så kaster den her inde i et `$effect`. En effekt, der kaster, tager komponentens opdateringer med sig: den forrige note bliver stående, og *ingen* note kan åbnes bagefter. Klikket kan ikke engang lande, hvilket er derfor det ikke ligner en fejl i noten, man klikkede på, men i hele ruden. To ting, ikke én. Værnet, så en manglende krop er en tom note. Og en ramme om tegningen, så det næste, der en dag ikke kan tegnes, heller ikke bliver til det her: en note, der fejler, viser sin egen tekst uden formatering, og resten af ruden virker videre. `loadedId` sættes nu før tegningen frem for efter. Ellers ville effekten prøve igen ved hver eneste ændring bagefter og kaste hver gang. `textContent` og ikke `innerHTML` i faldbakken: teksten er ikke tegnet, og at lægge den ind som markup ville være at køre præcis det, tegningen nægtede. Gemningen er ikke i fare — den skriver `draft`, som kommer fra svaret og ikke fra det, der står i editoren, indtil nogen taster i den. Prøven tvinger formen frem i stedet for at vente på den nyttelast, der gjorde det. Hvad der end fik `body` til at mangle, er svaret det samme: én note må ikke kunne låse de tolv hundrede andre ude. Den er set fejle uden rettelsen — med netop den fejl, konsollen viste. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>v0.34.2 |
||
|
|
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 <noreply@anthropic.com>v0.34.1 |
||
|
|
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 <noreply@anthropic.com>v0.34.0 |
||
|
|
8ecefbbdc2 |
fix(notes): rullebjælken svævede midt i arket
Det er `.page`, der ruller — og det var også `.page`, der blev smallere for at holde linjelængden nede. Så bjælken fulgte med ind til tekstsøjlens kant og stod med en håndflade tom rude til højre for sig, en tredjedel inde i ruden. Grænsen sættes som polstring i stedet for som bredde. Elementet er lige så bredt som ruden — bjælken bliver ved kanten, hvor den hører til — og teksten er nøjagtig lige så bred som før. `max()` mod nul, fordi en smal rude ellers ville få negativ polstring, og `border-box` er sat globalt, så bredden er hele boksen og ikke boksen plus polstringen; uden det ville det give en vandret rullebjælke. Kildevisningen deler `.page` med editoren og havde samme fejl. Den følger med. Målt: elementet slutter nu på rudens indholdskant i stedet for 74 px inde, og `scrollWidth - clientWidth` er nul. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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 <noreply@anthropic.com> |
||
|
|
e46aaa462a |
feat(projects): sektioner kan trækkes op og ned, og båndet står ikke længere skævt
Rækkefølgen af sektioner *er* planen — "Planlægning" står før "I gang", fordi det er den vej arbejdet går — og indtil nu kunne den kun laves om ved at slette en sektion og lave den igen, hvilket smider dens opgaver ud i det usektionerede område på vejen. Hele overskriften er håndtaget. En sektionsoverskrift indeholder ikke noget, der selv bliver trukket, så der er ingen geste at stjæle — hvilket er grunden til, at sidebjælkens rækker skulle have et greb, og grunden til at disse ikke skal. Ikke draggable mens den omdøbes: et draggable element lader ikke markøren vælge tekst i det felt, det indeholder. Serversiden er hele listen på én gang som projekter og grupper, og af samme grund: et projekt har en håndfuld sektioner, så 0, 1, 2 … er én lille transaktion uden præcision at løbe tør for, og en liste kan ikke ende halvt anvendt. `ReorderSections` er scopet til projektet og ikke til en ejer — en sektion har ingen ejer, den hører til et projekt, og om du må rykke rundt er et spørgsmål om det projekt, som handleren allerede har stillet. Id'er fra et andet projekt springes over af WHERE-klausulen; der er en prøve på præcis det, fordi ingenting ellers kontrollerer id'erne enkeltvis. Båndet stod fire pixels skævt. Det var trukket ud med --s2 og polstret med --s3 — samme idé gjort med to forskellige tal — så det blev otte pixels bredere end listen i hver side, og teksten landede fire pixels uden for den kolonne, rækkerne holder. Til venstre var det usynligt. Til højre var det Omdøb og Slet, der stod fire pixels forbi alt over og under sig, hvilket er præcis langt nok til at ligne en fejl og ikke langt nok til at se hvorfor. En række er `.row` med `padding: var(--s2)` inde i en boks uden, så båndet har nu samme polstring og ingen margen. Board-visningen er ikke rørt: der er kolonnerne vandrette, og det er en anden geste. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |