Two more things the database has been recording and the interface has
never shown.
**Who a task is waiting on, on the row.** Only when it is not you: your own
list would otherwise carry your own face on every line, which is a column
of one initial telling you nothing. The point of showing an assignee is
that it is somebody else.
That needed names, and a task carries only an `assignee_id`. `GET /people`
is the primitive: everybody you share a project with, plus yourself,
fetched once per session. Not per row, and not per project either — a list
would have to know which project each task belongs to before it could ask
for that project's members. Deliberately *not* the instance's user list,
which is `/users` and administrators only: a task list has no business
enumerating everybody with an account here.
It sits first in the meta line, because who changes what the rest of the
line means — a date on somebody else's task is their deadline, not yours.
**And the activity log.** Nineteen kinds of entry have been written since
the beginning — every project, section, member, task and comment event —
and `GET /projects/{id}/activity` has answered honestly the whole time with
nothing on the other end. There is a Historik panel on the project now,
loaded when it is opened rather than with the page, and the event keys
become Danish in exactly one place. An unknown key falls through to itself
rather than being hidden: a build that records something this table has not
learned should still show that it happened.
One test assertion had to change, and it is worth saying why. It matched
"males inden fredag" against a row whose title is "males inden" — quick add
read "fredag" as the due date — so it was really matching the title
concatenated with the metadata beside it. Putting the assignee between them
broke it. The assertion now names the task's own text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TaskRow has had an onedit prop that nothing passed. There was no way to
see a task's description, sub-tasks, comments, files or reminders, all of
which the API has supported from the start.
A drawer rather than a page, so the list you came from stays behind it
and ticking a sub-task off and closing lands you where you were rather
than at the top of a re-fetched list. Mounted in the root layout and
driven by an id in the store, so a task opens the same way from Today, a
project, a label or a saved filter — and so an edit arriving over the
socket reaches the drawer without a second source of truth to keep in
step.
Text fields save on blur. A PATCH per keystroke is forty requests for one
sentence, and the last to arrive is not necessarily the last one sent.
The navigation shortcuts are suppressed while it is open: with a task in
front of you, "t" means the letter t.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The board is one column per section with native drag-and-drop, which
works with a trackpad, a mouse and a screen reader without any of them
being reimplemented. Dropping is optimistic: a card that snaps back for
200ms reads as a failed drop.
Weeks in the calendar start on Monday. That is ISO 8601 and what every
Danish calendar does; a Sunday-first grid makes a Dane misread the month
at a glance. On a phone the chips give way to coloured bars — at that
width the month's shape is the useful part, not the titles.
Label pages run the filter language rather than a bespoke endpoint, so a
label page and a saved filter can never disagree about what a label
selects.
fix(recurrence): "hver mandag", not "hver uge mandag"
A weekly rule that names its days does not also need the word for the
interval. That is how the phrase is said, and it is what the parser
accepts back — the round trip through Describe now holds for weekday
rules too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The interface for phase 1: sign-in and first-run setup, Today, Upcoming,
project views, sharing, Cmd+K search and the keyboard shortcuts.
Quick add highlights what the parser understood underneath the text as
you type — a mirrored layer behind a transparent input, sliced by byte
offset because the server counts bytes and JavaScript counts UTF-16
units, which disagree on exactly the Danish letters this app is full of.
Every local action is optimistic and reconciled afterwards: ticking a
task off strikes it through and animates it out before the request
leaves, and rolls back with a message if the server disagrees.
The Content-Security-Policy is computed at startup by hashing the inline
scripts in the built index.html. Both of them change on every frontend
build, so a hard-coded hash would go stale silently and show up only as
a blank page in production. The alternative, 'unsafe-inline', would drop
the one protection that makes an injected script in a task title inert.
Today is derived from the store by date rather than from the ids that
came back with the first request — filtering against a captured list
meant a task added through quick add never appeared until a reload.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>