a71af41d9ecf321815f90cce0ca5471152eb56bc
10
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>
|
||
|
|
3132bba7e5 |
feat(data): hele kontoens opgaver kan hentes, og en indsat liste bliver til opgaver
To ønsker, meldt sammen. --- eksport af det hele --- Eksporten fandtes allerede — konto som JSON, noter som Markdown i en zip, og pr. projekt CSV i Todoist-format og ICS. Hullet var, at opgaver kun kunne hentes ÉT PROJEKT AD GANGEN. Med femten projekter er det femten downloads for at flytte sine egne opgaver, hvilket er en eksport, der teknisk set findes, og som ingen bruger. `GET /export/projects.zip` er én CSV pr. projekt. En zip og ikke én bred fil med en projektkolonne: Todoists egen eksport er en fil pr. projekt, så det er den form, der importeres i den anden ende — og den form, man kan hive ét projekt ud af uden at åbne et regneark. Samme begrundelse som noteeksporten, der er en mappe med Markdown og ikke én lang fil. `GET /export/tasks.ics` er alle daterede opgaver i én kalender. Ét fil og ikke en zip, fordi en kalender er lavet til at rumme mange ting, og projektet følger med hver opgave. Udaterede udelades — en VTODO uden dato er en linje, de fleste læsere taber på gulvet. Prøven ser I filerne frem for på statuskoden: en tom zip og en kalender uden VTODO'er er begge 200 med en troværdig Content-Type, og det er præcis den form, en fejl i en eksport har. To projekter, ikke ét, så en løkke der kun eksporterer det sidste ikke består. --- en indsat liste bliver til opgaver --- Quick-add er ét felt på én linje, og browseren smider linjeskiftene væk ved indsætning i et `<input>` — så en kopieret liste blev til én opgave med punkterne mast sammen. Nu læses den. Indrykning bærer niveauet, og bredden læses af teksten frem for at være gættet til to eller fire: en liste fra Apple Noter rykker ind med ét antal mellemrum, en fra et Markdown-dokument med et andet, og begge skal give ét niveau. `- `, `*`, `1.` og `1)` fjernes som den notation, de er. `- [x]` er derimod indhold: en halvt afkrydset liste, der kommer ind som lutter uløste opgaver, har tabt det, man indsatte den for. Teksten røres ikke ud over mærket. Datoer, `#projekt` og `@etiket` læses af quickadd på serveren, præcis som når linjen var skrevet i hånden — ét sted at forstå en opgavelinje, ikke to. Derfor tager `/tasks/quick-add` nu et `parent_id`: en indrykket linje skal parses som enhver anden. En undertask lever i sin forælders projekt. Et `#projekt` på en indrykket linje honoreres derfor ikke, og det siges højt som `unknown_project` frem for at ordet bare forsvinder fra titlen. Indsætter man i en opgaves undertask-felt, bliver linjerne undertasks dér. Det er "enten opgaver eller undertasks" afgjort af hvor man indsætter, uden at nogen skal vælge noget i en menu. Listen vises, før den oprettes. Fyrre opgaver fra et fejlklik er ikke noget, man skal opdage bagefter — og visningen er også svaret på "forstod den indrykningen", stillet mens der stadig er en Fortryd-knap. --- rettet undervejs --- Rækkens undertask-tæller stod stille, når en undertask blev tilføjet fra ruden: rækken sagde 1/2, mens ruden foran viste fire. Tallene bor på forælderen og regnes ud ved hentning, og serveren sender ikke forælderen igen, fordi et barn blev lavet. Det gjaldt også den gamle vej, ikke kun den nye. Go-suiten grøn. Fuld chromium-suite: 103 bestået, 0 fejlet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ab77024ec9 |
feat(beskeder): klokken siger til, når nogen rører noget, der er dit
En delt note blev rettet af en anden, og man opdagede det kun ved at åbne den. En opgave blev givet til en, og den lå bare der. Det, andre gør ved det, man deler, skal man kunne se uden at lede efter det. Klokken i hovedet: ulæste beskeder om noter, der er blevet rettet, delt med en, eller opgaver, man har fået. Sætningen skrives i browseren, ikke på serveren, så et engelsk interface får en engelsk klokke. Beskeder om den samme note bliver slået sammen, mens de er ulæste. En note gemmes på en tastepause, så en række pr. gem ville lave én person, der skriver i fem minutter, om til tyve ens linjer. Er beskeden læst, er den næste rettelse en ny ting at få at vide — ikke en gentagelse. note_id får sin egen søjle ved siden af project_id og task_id frem for et generisk (kind, id)-par: de tre er ikke det samme slags nul, og fremmednøglen — dén, der rydder beskeden op, når noten slettes — kan et generisk par ikke have. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9e3f20b050 |
feat(tasks): snooze — park a task at the bottom until a time
Snooze puts a task out of the way without finishing it or filing it: it sinks to
the bottom of its list and greys, and comes back on its own when its time passes.
No job wakes it — "snoozed" is just snoozed_until being still in the future, read
against the clock each time the list is ordered.
- `snoozed_until` on tasks (migration 0031); the list order sinks a still-snoozed
task below everything, and it rejoins its place the moment the time passes.
- POST /tasks/{id}/snooze {until} — an RFC3339 time to snooze to, or empty to wake.
Nothing else about the task moves; a snooze is about when you want to see it, not
when it is due.
- The task drawer gains quick picks (this evening / tomorrow / next week) and a
custom time, and a Wake now once it is snoozed. The row dims and carries a
"snoozed until X" mark, and the list keeps snoozed tasks at the foot — the client
sorts the same way the server does, so a drag-sorted list does not jump on reload.
Tests: snooze sinks a task below one made after it and waking returns it (Go); and
from the drawer the row greys, shows the mark, and un-greys on wake (e2e). Full
suites green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
0dbf7a40bf |
feat(tasks): write a task straight into a section
The box at the top puts things in the project with no section, which is right for capturing. But a project has sections because the work belongs somewhere, and dragging every new task down into the right one is a second gesture for something you already knew when you typed it. Two ways in, because they answer different moments. A field at the foot of each section and each board column — standing there is enough, no syntax to know. And `/section` in quick add, for the box at the top and for Today and Upcoming, where you are not standing in a section at all. The field goes through quick add rather than createTask, so a line written there still parses its dates, priorities and labels. The sigil needed care: '#' and '@' never turn up in a Danish task line by accident and '/' turns up constantly. It has to follow whitespace and be followed immediately by the name, which "km/t", "3/4" and "20 kr / stk" all fail. The bare form is greedy, because "Kunder/Ordrer" is a real section name and the sigil is inside it. A section id from the client is checked against the project the task actually landed in. A `#project` in the same line can move the task elsewhere, and the id would otherwise follow it into a project that section does not belong to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
101ca33069 |
feat(tasks): a row says what is underneath it, and what is attached
A task with three sub-tasks looked exactly like one without, and so did a task with a drawing on it. The whole reason to attach something is that somebody should find it. Counted in the same query as the tasks, as correlated subqueries: a list of fifty would otherwise be a hundred more lookups, and the point is that you see this without asking. Said as a fraction rather than a total. "3" tells you something is there; "1/3" tells you whether there is anything left to do, which is the question a list is for. All-done goes accent-coloured, because "3/3" and "1/3" are the two answers you actually look for and only one of them is good news. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
a96aae57ec |
feat(recurrence): repeating tasks as RRULE, in Danish and English
"vand planterne hver mandag" becomes FREQ=WEEKLY;BYDAY=MO. The stored form is RFC 5545, not a private format, so the same field will drive the CalDAV server and the ICS feed without a translation layer. Ticking off a repeating task advances it instead of closing it, keeping the same row — so its sub-tasks and comments stay attached. A weekly review is one thing that recurs, not fifty-two things. The clock time survives the move, and the completion is still written to the activity log, because "what did I get done this week" has to include the chores that repeat. Recurrence is parsed before dates, and that ordering is load-bearing: "hver mandag" contains a weekday, so a date parser reading it first would claim Monday as a one-off and strand "hver" in the title. It is also resolved before the time-only case, or "hver mandag kl 9" typed after nine would land tomorrow morning instead of on Monday. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
dff86dd23d |
feat(api): projects, sections, tasks, quick add, views and live sync
Completes the MVP surface: project and section CRUD, task CRUD with sub-trees, drag-and-drop ordering, the quick-add endpoint over the natural-language parser, Today and Upcoming, search, sharing with roles, an activity log and a WebSocket for live updates. Permission checks are route middleware rather than a first line in each handler, so an endpoint is guarded by where it sits instead of by the author remembering. Anything the caller may not see answers 404, never 403 — a 403 confirms the thing exists. Visibility is enforced inside the list query, so another user's project cannot be reached by filtering or searching at it. Ordering is a float, so a drop writes one row instead of renumbering the list. When repeated drops into one gap exhaust the precision, the section is spread out again and the move retried — in a separate transaction, since the attempt that discovered the problem rolls back. The connection pool is not 1: a single connection makes the pool a lock, and a query run while an earlier result set is still open then waits for a connection it is itself holding. The store now closes its rows before querying again, and the pool leaves that mistake slow rather than fatal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |