956e57f234586a731080bfb09fb8343b4ae97e5a
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
956e57f234 |
feat(opgaver): alarmen kan tales ind, og kan være standard
To halvdele af det samme: at en opgave med et klokkeslæt også kan få en
alarm, uden at man går en omvej hver gang.
Påmindelser har virket længe og skulle sættes én ad gangen. En opgave
dikteret som "i morgen kl 9" fik en tid og INGEN alarm — `CreateReminder`
blev kun kaldt fra sit eget endepunkt, så intet skabte en påmindelse af sig
selv. Derefter skulle man åbne opgaven, finde Påmindelser og vælge et
klokkeslæt, og den omvej er længere end at skrive opgaven.
SÆTNINGEN
Hurtig tilføjelse forstår nu alarmen:
ring til tandlægen i morgen kl 9 #hjem mind mig en time før
Formerne er `mind mig` / `påmind` / `remind me` / `med alarm`, efterfulgt af
en varighed (`en time før`, `10 min før`, `et kvarter før`, `en halv time
før`, `dagen før`, `to dage før`) eller et klokkeslæt (`mind mig kl 8`).
Bar "mind mig" betyder på selve tidspunktet. Dansk og engelsk i samme
linje, som resten af parseren.
Det gælder uanset hvor stemmen kommer ind: tastaturets diktering, en
genvej, eller noget tredje ender som tekst gennem den samme parser. Derfor
er syntaksen forudsætningen for dem alle frem for én af dem.
Trinnet ligger FØR klokke og dato, og den rækkefølge er hele tricket: "mind
mig en time før" har en varighed i sig og "mind mig kl 8" har en klokke, og
begge læst af tidsparseren først bliver opgavens egen forfaldstid i stedet
for dens alarm.
STANDARDEN
En indstilling under Notifikationer giver opgaver med både dato og
klokkeslæt en alarm uden at blive bedt om det, med et valgfrit varsel.
Den rammer alle tre veje, en opgave kan få et klokkeslæt — oprettet,
redigeret og hurtig-tilføjet. Redigeringsvejen er den halvdel, løftet om
"alle opgaver med en dato og tid" ellers taber: en opgave skrevet nu og
givet en tid i eftermiddag er den samme opgave. CalDAV er bevidst udenfor:
en opgave fra Apple Påmindelser bærer sin egen alarm, og to alarmer for én
opgave er værre end ingen.
Slået FRA for alle, der findes i dag. At tænde den i en migrering ville
give hver eksisterende bruger alarmer, de aldrig har bedt om, på opgaver de
har liggende — og det her er software, andre selv driver.
Og standarden skabte et behov for en udvej, så den fik en: "ingen alarm" i
linjen springer en enkelt opgave over, mens man skriver den, frem for at
oprette den, åbne den og slette alarmen der lige blev lavet. En udtrykkelig
alarm slår også standarden — den lægger sig ikke oven på.
TRE TING LÆSNINGEN FANGEDE
* Fortegnet. Lageret LÆGGER forskydningen til forfaldstiden
(`due_datetime + offset_min * 60`), så "en time før" er -60. Gættet
havde givet en alarm en time for sent, og det er den ene retning en
påmindelse ikke må tage fejl i. Samme konvention hele vejen —
indstillingen, parseren og kolonnen — for to fortegnsregler for det
samme begreb viser sig kun som en alarm på et forkert tidspunkt.
* Den stille døde alarm. `DueReminders` vælger forskydningsformen kun
`AND t.due_datetime IS NOT NULL`. Så "mind mig en time før" på en opgave
med en dag men ingen klokketid ville have lagt en række, der aldrig
fyrer. Nu bliver opgaven oprettet, alarmen ikke, og det bliver sagt.
Det er den ene slags fejl en påmindelse ikke må have, fordi intet
bagefter afslører den: der står ingen række at savne.
* "-60 min." på skærmen. TaskDetail viste forskydninger råt. Før nu kunne
de kun laves gennem API'et, så grenen stod reelt urørt — dette er det
første, der bruger den i praksis. Den skriver nu "1 time før".
PRØVET
Prøverne spørger `DueReminders` frem for om rækken findes: en række med
forkert fortegn findes lige så overbevisende og ringer en time for sent. Et
minut før den er forfalden må alarmen ikke være i listen, et minut efter
skal den.
Hver plantet fejl er kontrolleret til at falde i præcis den prøve, der
findes for den — fortegnet vendt, ordgrænsen fjernet, trinnet flyttet efter
dato, umulig klokke accepteret, dobbelt-vagten slået fra, "ingen alarm"
ignoreret, positivt tal accepteret, råt fortegn vist.
Og prøvens eget navn er en rettelse: de første fejlede i hele suiten men
bestod isoleret. Suiten kører med én arbejder mod ÉN database, og "ring til
tandlægen" findes fire steder i filen, så `.first()` ramte en anden prøves
opgave uden alarm.
Go-suiten grøn. E2E: 131 bestod, 0 netafbrydelser.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
7e981354c2 |
feat(indstillinger): en indstilling kan findes ved at søge efter den
Indstillingerne er ti faner med syvogtredive afsnit, og den eneste vej til et bestemt af dem var at huske, hvilken fane det lå under. ⌘K kunne opgaver, projekter og noter — alt det, man selv har skrevet, og intet af det, programmet består af. Ét indeks, to steder det bruges: et felt øverst i indstillingerne og paletten. To lister over de samme afsnit ville være to steder at glemme et nyt et. Overskrifterne står ikke i indekset. Hver post peger på den nøgle, afsnittets egen overskrift allerede bruger, så et afsnit, der bliver omdøbt, følger med af sig selv, og søgningen kan begge sprog uden at kende nogen af dem. `words` er derimod skrevet i indekset, og det er med vilje: det er søgeord, ingen får at se. Det danske afsnit hedder "Tælling af installationer", men det ord, man kender det ved, er "beacon" — og på samme måde 2fa, imap, webcal, webhook. De er de samme på begge sprog, hvilket er hele grunden til, at de virker som søgeord. Ankrene er nye id'er på afsnittene. Tre af dem stødte sammen med et felt inde i afsnittet — to ens id'er gør <label for> tvetydig og getElementById tilfældig — og hedder derfor to-faktor, kalenderfeed og caldav-server. Springet dertil kan ikke overlades til browseren: siden hentes af ruteren, og afsnittet findes først, når dens data er hjemme. Der prøves i to sekunder og gives så op. Blinket bagefter er ikke pynt — man er landet midt i en side med otte afsnit, man ikke selv rullede hen til. Prøverne læser filerne frem for at klikke: halvdelen af afsnittene vises kun under en betingelse, så en prøve, der ledte efter dem i en browser, ville melde fejl på det, der med rette var skjult. Og palettens pladsholder sagde "Søg i opgaver og projekter". Den har kunnet noter længe, og kan nu også indstillinger. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8139003b35 |
feat(ai): dagens plan, mails der giver mening, og svar på et spørgsmål
De tre sidste af de fem. Alle tre samme sted hen: det, man ellers selv skulle sidde og gøre klokken syv om morgenen. Dagens plan. Én besked om morgenen om det, der forfalder i dag, og det, der er sprunget over. Tallene tælles i basen og gives til modellen som kendsgerninger, den ikke må regne om — en plan, der siger fire opgaver, hvor der er tre, er værre end ingen plan. Er der ingen model, står tallene alene, og det er stadig det, man havde brug for at vide. Er der hverken noget, der forfalder, eller noget, der er sprunget over, kommer der ingen besked: en morgen uden noget at gøre er en god morgen, og en besked om ingenting lærer folk at lade være med at åbne beskeder. Timen er personens egen — klokken syv er syv forskellige steder — og jobbet kører hver time frem for på et klokkeslæt, så en instans, der sov, ikke springer dagen over. Flytter man timen, ryger dagens mærke, så man kan se virkningen i dag i stedet for i morgen. Mails, der giver mening. Opgaven hed, hvad mailen hed: "Anders Jensen: SV: SV: Vedr. levering uge 12". Nok til at genkende, ikke nok til at gøre noget ved. Nu skriver modellen den handling, mailen beder om, og lader være, hvis den ikke beder om nogen. Ét kald pr. gennemløb frem for ét pr. mail — femogtyve mails er ellers femogtyve gange ventetid — og emnet bliver stående i beskrivelsen, så en misforstået mail stadig kan læses. Falder modellen ud, hedder opgaven afsender og emne som før: en model, der er nede, må ikke kunne stoppe posten. Og svar på et spørgsmål. "Hvad lovede jeg Anders i august?" — svaret står spredt over tre noter, og ingen af dem indeholder ordet "lovede". Søgningen kræver, at alle ord optræder, hvilket er rigtigt for et søgefelt og forkert for et spørgsmål, så kandidaterne hentes ord for ord og de noter, flere ord rammer, kommer øverst. Modellen får kun dem: den slår ikke selv noget op, så et svar kan ikke handle om nogen andens noter. Og svaret bærer, hvad det står på — en model, der siger "det gjorde du den 14.", er kun brugbar, hvis man kan slå op i det, den læste det i. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
fcd6dca852 |
fix(push): "der kommer ikke nogen" kunne ikke besvares
Release / Build and publish image (push) Failing after 10m0s
Alt om det var usynligt. Indstillingssiden sagde "til for denne enhed", så snart der fandtes et abonnement — også når hver eneste push blev afvist. Og en afvist push gik i en logline på **Debug**, som ikke skrives på en instans, der kører på standardniveau. Så en levering, der fejler lydløst, og en levering, der aldrig sker, så ens ud, og forskellen er hele diagnosen. Tre forskellige problemer havde ét udseende: browseren tilmeldte sig aldrig, tjenesten afviste den, eller der var ikke noget forfaldent endnu. **En prøveknap** under Indstillinger → Notifikationer sender én notifikation gennem den rigtige rute — ikke en genvej, så det, den beviser, er det, der faktisk skal virke — og siger hvad hver enhed gjorde ved den. Kom den ikke frem, og afviste ingen den, står der, at det så er browseren eller styresystemet, der holder den tilbage. Nu peger svaret på ét af de tre steder. Loglinjen er hævet fra Debug til Warn. Det var den ene linje, der ville have sagt hvorfor. Teksten kommer fra klienten, fordi ordbøgerne gør. En server, der bærer sin egen kopi af "det her er en prøve", bærer den på ét sprog. **Og en lækage, prøven fandt.** Go pakker en transportfejl som `*url.Error`, og dens tekst er `Post "https://fcm.googleapis.com/fcm/send/dQw4…": dial tcp: …` — hele endepunktet, som er den legitimation, push-tjenesten udstedte til den enhed. Den var på vej ud i svaret, i loglinjen og i ethvert skærmbillede af nogen af dem. Prøven var skrevet for at bevise, at kun værten blev nævnt, og det var sådan den blev fundet. Fejlen pakkes ud, og adressen skrubbes bagefter alligevel: den næste fejltype, der bærer den med, kommer ikke med en advarsel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2b45aa33e1 |
feat(admin): ask the panel to restart, from verdande's own settings
Release / Build and publish image (push) Failing after 10m2s
A container cannot replace its own image from the inside, so "update" is really "ask whoever owns the container to recreate it" — Yggdrasil pulls `:latest` on the way, which is what makes a restart an update at all. This removes the second browser tab, not the panel's authority: the panel still does the work and still records who asked. Three settings, needed together, and the page names whichever are missing — an operator reading "not configured" should not have to go and find out which of three. The token is never sent to a browser; the page is told only whether it is set. Answers 202 rather than waiting. The panel stops this container as part of answering, so the reply may never arrive, and a request cut off mid-flight is the successful case wearing a failure's clothes. The interface believes neither answer and polls /healthz until it responds. Administrators only, and sessions only. A leaked API token that could restart the server is a denial of service in one request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ab5e3e35a6 |
fix(i18n): three Danish strings that had no æ, ø or å in them
Spotted on the running instance: the version line read "Running v0.11.0. Der bliver ikke set efter opdateringer." — half translated, on one line. The sweep that was supposed to catch these grepped for Danish letters, and none of the three survivors contain one: "Ingen nyere version fundet.", "Ikke mere i dag." and a token hint split across two source lines so neither half looked like prose. So there is a test for it now. It reads the templates and looks for the words that give Danish away whatever letters they are spelled with — a word list rather than a language detector, because this ships as one static binary and thirty common words catch the sentences a person actually writes. It fails on the exact three that got through. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4d8b70918b |
feat(i18n): the interface speaks both languages
The account has had a `locale` since the beginning and it only chose which grammar quick add read a line with — so picking English changed nothing anybody could see, and the field said so honestly: "Sprog i hurtig tilføjelse". It says "Sprog" now, because it means it. Every component goes through `t()`. Danish is the source: a key missing from English falls through to Danish rather than to the key, because a Danish sentence in an English screen can be read and `settings.notifications.title` cannot. `tag()` is the single place `Intl` is asked. Before, `'da-DK'` was written into a dozen components, and that is the half of a translation that gets left behind — an English interface still saying "24. aug." is exactly the shape of a job half done. There is a smoke test that switches to English and refuses to see a Danish month name in the calendar's heading. Colour and theme names are keys now. `colors.js` keeps its ids untouched — they are what the database stores, and `internal/store` reads them out of that file — but `name` is `'color.tomato'`, resolved where it is drawn. The Inbox keeps its name. It is a project row the server creates at signup, not a string in the interface, and it can be renamed like any other project. Four copies of "3 hours ago" became one, in `web/src/lib/when.js`. They had already drifted: one said "aldrig logget ind", another "aldrig", and two stopped at days where the others carried on to a date. Routes stay Danish. `/indstillinger`, `/projekt`, `/uddelegeret` are bookmarks and they are in every smoke test; translating them buys nothing and costs both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9d9444d0ac |
feat(web): the settings surface
There was no /indstillinger route at all. The Gmail OAuth callback redirects there and had been answering 404 since it was written. Six pages rather than one long scroll, because the sections have nothing to do with each other and somebody opening API tokens should not have to walk past their own password to get there: the account and its second factor, notifications, integrations, AI, tokens, and data. Everything listed as having no interface in the handoff is reachable from one of them — the calendar feed, the mail-to-task address, Gmail, the AI provider, import, export, templates and the update notice. Two decisions worth naming. The AI key field starts empty and says why: a settings page that repopulates a password field is one that will eventually leak a key into a screenshot, and an empty value means "leave the stored one alone" rather than "clear it". And the form chrome lives in the layout as :global rules, because scoped styles do not reach a child route and six copies of the same twelve selectors is six places for them to drift. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |