Commit Graph
8 Commits
Author SHA1 Message Date
kristianwindandClaude Opus 5 8c3aa33f13 fix(udgaver): porten dækkede 29 ruter og skulle dække 63 — og fladen ringede stadig på dørene
Prøven, der skal holde udgaverne adskilt, stod grøn gennem to merges, mens
34 opgaveruter blev betjent af noter-udgaven. Den fejlede ikke: den
sammenligner routeren med en nedskreven liste, og listen var skrevet ud
fra de ruter, nogen havde tænkt på. Den er en DRIFT-kontrol, og den kan
ikke være en fuldstændighedskontrol. Det står nu i prøven selv.

Lukket: CalDAV-træet (8 ruter, alle læser eller skriver en VTODO),
.well-known/caldav (en påstand om at der er en CalDAV-server), ICS-feedet
og de to ruter til dets token, de fire opgave-eksporter, Todoist- og
CSV-importen, skabelonerne, sektionerne, de seks AI-opgaveruter og
indbakke-krogen. Plus fem af ni MCP-værktøjer — endepunktet er der
stadig, værktøjslisten er kortere.

Tre blev afgjort ved at læse frem for ved navnet: en skabelon laver
opgaver (CreateProjectFromTemplate), `section_id` står på `tasks` og
findes ikke på `notes`, og add_comment hænger på en opgave.

Men den dyre halvdel var fladen. En vandring gennem hver side, der kan
nås i noter-udgaven, med konsollen og svarene aflæst, fandt SYV brud —
fire af dem fra den første port:

  sidebjælken      GET /filters  404 ved hver navigation, ædt af .catch()
  sidebjælken      GET /labels   404 ved hver navigation, ædt af .catch()
  notifikationer   GET /ai/plan  404, stille
  notesbogens side GET /tasks    kastede → status = 'failed'
  notesbogens side GET /sections kastede → samme fejlskærm
  indstillinger    GET /templates  toast med fejlbesked på skærmen
  integrationer    GET /feed     404, stille

Den fjerde er den, der har kostet noget: /projekt/{id} er notesbogens
side, og kaldene samles i et Promise.all, så én 404 tog hele siden med.
Den har vist en fejlskærm frem for noter siden porten blev sat. Ingen
prøve åbnede en notesbog i den udgave, og .catch(() => {}) sørgede for,
at resten ikke sagde noget. "Fladen følger udgaven" var sandt om menuen
og falsk om hentningerne.

Vagten for opgaver står i butikken frem for ved kaldestederne: der var
tre, og den fjerde blev glemt — hvilket er præcis sådan, den slap
igennem.

Og rettelsen lavede selv en fejl, som vandringen fangede: da opgavekaldet
holdt op med at fejle, kom en uendelig effekt-løkke frem, som 404'eren
havde holdt låg på. En ubetinget tilskrivning til app.tasks får
projektsidens effekt til at kalde tilbage. 404'eren var låget på en
løkke, ikke bare støj.

Også: -timeout 20m i begge CI-workflows. internal/httpapi tog 664s her og
498s i CI på #95, mod Go's standardgrænse på 600 — og når den krydses, er
beskeden "panic: test timed out after 10m0s" plus et goroutine-dump, som
læser som en deadlock i koden frem for som en suite, der blev for stor.

watchForTrouble udskilt til web/e2e/trouble.js, så udgave-suiten kan bruge
den samme.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-07 02:03:40 +02:00
kristianwindandClaude Opus 5 d8f5f6e236 feat(noter): nævn en person i en note, og den er delt med dem
At skrive nogens navn i en note *er* at ville have dem til at læse den.
Indtil nu var det to handlinger — skriv navnet, åbn panelet, find
personen, del — og den anden blev glemt, så noten stod med en besked til
en, der ikke kunne se den.

Nu deler `@Navn` noten med den, der bliver nævnt, og siger det til dem.
Editoren foreslår personer efter `@`, ligesom `#` foreslår projekter.

Slået op i navnene frem for tolket ud af teksten. Et navn kan have
mellemrum i sig, og et udtryk, der skulle fange dem alle, ville også
fange sætningen bagefter: `@Sofie i morgen` ville blive til en person,
der hed noget i den retning. Her er der ikke noget at gætte — der står et
navn, eller der gør ikke. Det er også dét, der gør det forsvarligt at
dele på: adgang må ikke afhænge af, hvor langt et regulært udtryk valgte
at læse. En mailadresse er ikke en omtale, det længste navn vinder, og
ejefaldets s hører til sætningen.

Kun nye omtaler, sammenlignet med teksten før gemningen på samme måde som
links: en note gemmes ved hver tastepause, så uden sammenligningen var
hver gemning en ny deling og en ny besked om det samme navn.

Kun ejerens omtaler deler. En medredaktør må skrive i noten, men ikke
give den videre — ellers kunne den, ejeren har delt med, skrive et navn
og dermed dele ejerens note med en, ejeren aldrig har hørt om. Det er
den samme regel, panelet følger.

Læser, ikke medredaktør: en omtale siger "se på det her", og det mindste,
der opfylder sætningen, er det rigtige at give. En rolle, ejeren har
hævet i panelet, sænkes ikke igen af en omtale.

Og én besked pr. handling: den, der lige er blevet nævnt, får "nævnte
dig" og ikke også "rettede en note" om den samme gemning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 12:08:12 +02:00
Kristian WindandClaude Opus 5 5e670119db feat(notes): titlen er første linje, og # foreslår projektet
Release / Build and publish image (push) Failing after 9m59s
Titlen var udledt én gang og derefter ladt i fred, så den gik i stå i det øjeblik
nogen skrev begyndelsen om — en note med overskriften "Ny note" stod i listen som
"fdgfgfgh", hvilket var, hvad dens første linje havde været nogle rettelser
tidligere. Den udledes nu ved hver gemning, og ingen kan sætte den ved siden af
teksten: kolonnen er en kopi af kroppen, ikke et felt derudover. Feltet er væk fra
både HTTP-fladen og MCP-værktøjerne, for et felt, serveren ignorerer, er et løfte,
den ikke kan holde.

Skriver man #Kaf, foreslås #Kaffebaren. Ordet læses fra den tekstknude, markøren
står i, og ikke fra hele dokumentet — en note kan nævne tyve projekter, og kun det
under markøren er ved at blive skrevet. Listen ejer piletasterne og retur, mens
den er åben, som enhver anden fuldførelse.

Og under feltet står nu, hvad teksten forstår: # projekt, [[en anden note]], ⌘B,
⌘U. Samme idé som linjen under hurtig tilføjelse — en note er det andet sted i
programmet, hvor det, man skriver, bliver læst for betydning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 18:36:31 +02:00
Kristian WindandClaude Opus 5 5f04fe56bc feat(notes): note-værktøjer i MCP, og sidebjælken ryddet op
Claude kan nu skrive og søge i noter gennem MCP. Det er også svaret på Apple
Noter: der findes ingen eksport, der er værd at bruge, men en model, der kører på
maskinen, kan læse Notes.app og skrive det, den finder, ind herigennem — én note
ad gangen, med teksten som Markdown. En flytning, ingen skal bygge en parser til.

Værktøjet melder tilbage, hvilke links noten fik. En note, hvis #tag var stavet
forkert, ser ellers præcis ud som en, hvor det ramte, og i en chat ser ingen den
række, der blev skrevet.

Noter står nu for sig selv med en streg over. De er ikke en femte måde at se de
samme opgaver på, hvilket er hvad alt ovenfor er — de er den anden halvdel af
programmet, og i den række læste de som endnu et filter.

Færdige er ude af menuen og ligger under Indstillinger → Data ved siden af
papirkurven. Argumentet for at have den fremme var, at man ikke ved, hvilken
visning man lukkede noget i — det er sandt, og det er også et sjældent ærinde ved
siden af fire punkter, der læses hver dag. Den er der stadig, med en beskrivelse
af hvad den er til.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 13:33:54 +02:00
Kristian WindandClaude Opus 5 c2e2d66e76 feat(admin): keep server errors where a restart cannot take them
Release / Build and publish image (push) Failing after 12m56s
The panel's watcher reported four 5xx today — twice from one address at
11:49, twice from another at 10:45 — and there is no way to find out what
any of them were. verdande logs the real cause of every 500, but on a Rune
the log belongs to the container and the container is replaced on every
restart. So an operator is told that something broke and given nothing to
act on, which is the worst of both: alarming and useless.

Every 500 now writes a row: what the handler was attempting in its own
words, the error it hit, the method and path, who was asking, and the
request id that ties it back to the log line for whoever still has it. The
"what" is the part a status code cannot give — "list projects" and "move
task" are the difference between a fault you can find and one you can only
count.

Written best-effort and never checked. This is the path that is already
reporting a failure, and a diagnostic that can itself fail the request
would turn one broken screen into two.

Threading the request through `internal()` and `storeError()` touched 108
call sites across 19 files, which is the price of not having had it there
from the start. It is mechanical and the compiler checked all of it.

Kept for thirty days by the nightly sweep: long enough to answer "what
happened last week", short enough that a fault loop cannot fill the volume
the tasks live on.

Read at Indstillinger → Fejl, administrators only — it carries paths,
account names and the server's own error strings. The empty state says
"Ingen fejl registreret. Det er den rigtige tilstand", because a blank
panel should not read as a broken one.

The test drops a table and asks for the endpoint that reads it: the same
code path a disk error takes, and the row that comes back has to name the
operation, the account and the request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:58:02 +02:00
Kristian WindandClaude Opus 5 afc46a5c20 fix(api): do the things the API says yes to
Release / Build and publish image (push) Failing after 9m58s
Three reports from somebody driving the app through the MCP connector,
all the same shape: an argument accepted, an OK returned, and nothing
done.

**project_id on an update never moved the task.** Both the REST handler
and the MCP tool read it, both checked the caller's rights on the
destination — which reads as "understood" — and then neither passed it
on, because store.TaskUpdate has no such field: a project change is a
move, since sort_order is per project. So the task stayed where it was
and the answer said OK. Moving a task between projects was impossible
except by creating it again and closing the old one, which is what the
person ended up doing. Both paths now go through one helper, so they
cannot drift apart again.

**An unknown #project vanished without a word.** The parser reads
"#3Dekoration" correctly; the lookup then fails, the task goes to the
Inbox, and the name is gone from the title with nothing anywhere saying
why. Still not an error — a thought should not be thrown away over a typo
— but quick-add now returns unknown_project, and the MCP tool returns a
note written for a model to relay. The old behaviour was kind in the web
interface, where you see the row appear in the wrong place and fix it. In
a chat nobody sees the row.

**Sub-tasks were unreachable over MCP.** create_task had no parent_id,
though the REST API has had it all along, so importing a parent with
seven children produced eight unrelated tasks with the parent's name
prefixed onto each. A sub-task joins its parent's project — the two
cannot disagree — and a second level is refused rather than silently
flattened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:24:58 +02:00
Kristian WindandClaude Opus 5 e63d852847 feat(mcp): let the connector address carry its own key
verdande could not be added to Claude at all. The documented setup —
POST /api/v1/mcp with an Authorization header — cannot be configured from
Claude's custom-connector dialog, which asks for a name, a URL and OAuth
client credentials and has no field for a bearer token. The instructions
described something the interface does not offer.

Worse, the obvious guess failed silently in the most misleading way
available: /mcp was not a route, so the SPA fallback answered 200 with a
page of HTML. The connector reported a successful connection and then
failed to parse the response, which reads as a broken client rather than
a missing endpoint.

So /mcp exists now and takes its credential in the query string, exactly
as the calendar feed does and for the same reason: a client that cannot
be told to send a header means the address is the credential. The
request logger records paths without their query strings, so the key does
not reach the log.

It is deliberately outside /api/v1 and accepts the key and nothing else.
Reading a session cookie here would make it a state-changing POST outside
the CSRF check — a cross-site request that acts as whoever is signed in —
and there is a test that fails if that ever becomes true.

The API path is unchanged for anything that can send a header. The tokens
page now shows the connector address next to the token it belongs to,
since that is the one moment the token exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 14:04:03 +02:00
Kristian WindandClaude Opus 5 6b19ed062d feat(phase5): MCP server, CalDAV, mail-to-task, Gmail settings and an AI layer
The MCP server speaks JSON-RPC over Streamable HTTP with the six tools
the brief names, authenticated by a personal API token — so a connector
reaches exactly what its owner reaches and there is no second identity
to reason about. A failed tool call returns a tool error, not a protocol
error: a model can recover from the first and can do nothing with the
second.

CalDAV serves VTODO, two-way, so Apple Reminders and Thunderbird can
read, create and complete tasks. Clients authenticate with Basic using
an API token as the password, never the account password — a client
stores what it is given, and what it is given should not be able to sign
in to the app. Completed tasks stay in the collection: a client that
stops seeing one treats it as deleted and drops its own record of having
done it.

The AI layer is one interface with adapters for Anthropic, OpenAI,
Google and any OpenAI-compatible endpoint. That last one is why the
abstraction exists: an integration that only spoke to a hosted API would
be useless to somebody running their own models. Every feature degrades
to "off" when nothing is configured, and the stored API key is never
returned to the settings page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 07:53:53 +02:00