The sidebar redesign puts every project at the same left edge; flip the
assertion from "indented more than a fixed view" to "in line with it".
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Toward Things' calm: the fixed views carry icons instead of dots — a star
for Today, a calendar for Upcoming, a person for Waiting on others, a tray
for the Inbox. Areas (the user's groups) are set in bold, plainly cased, so
the eye lands on them the way it lands on an area in Things; the system
sections (Projekter, Filtre, Etiketter) stay quiet. And projects sit at the
same left margin whether or not they are in an area — one straight edge down
the sidebar rather than a staircase, with the bold heading doing the
grouping the indent used to.
Screenshots regenerated to match.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The sidebar read as muted 13px; the menu items now use their own size at
medium weight so they carry at a glance (Todoist-ish). Both sizes are
choices under Settings → Appearance:
- Menu font size drives a dedicated --menu-size the sidebar uses.
- Body text size re-scales the shared type tokens (the list and the notes),
the same mechanism a look uses.
The two are independent on purpose — one is a glance down a list of names,
the other a paragraph you read. Kept in localStorage like theme and look,
and applied in app.html before first paint so a chosen size does not flash
in and resize. Four steps each (Small/Default/Large/Largest); Default
carries no attribute so the base tokens stand.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Andreas signed up as "andreas" and was shown that way everywhere. A name
is now capitalised on the way in — the first letter of each word raised,
the rest left as typed — in CreateUser (signup and invite-accept) and
UpdateProfile, so display, mail and export all agree.
Existing accounts are fixed by an idempotent startup pass
(NormalizeUserNames), the same shape as SealNotes: the rule is Unicode-aware
and per word, which SQLite's UPPER cannot spell, so it lives in Go rather
than a migration.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The first cut put an open-button overlay over the row, which intercepted
every click on the title text — nine smoke tests (and real users) open a
task by clicking its text, and the overlay stole those clicks. Make the
body a role=button instead, with each link stopping its own click from
bubbling: plain text opens the task, a link opens the page.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A task is plain text — the title is an input, the description a textarea —
so a pasted URL just sat there as words. It is now caught at display time
and shown as a link in the list and on the board, without the stored text
ever being anything but plain text.
The link cannot live inside the row's open-button (an <a> in a <button> is
invalid), so the body is a container with the open action as an overlay
behind it: plain text and the meta row still open the task, links are
raised above and take their own click. Conservative about what counts —
an explicit http(s):// or a bare www. host, not every dotted word.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A donate link belongs in Settings and nowhere in the way — so it sits last,
understated, below the device list. A plain anchor with an inline SVG cup:
no third-party script, no webfont, no tracking, so it works offline and
cannot phone home. Wording says the app is MIT with nothing held back and
that nothing changes if you don't pay.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Yggdrasil is self-hosting too, so labelling only the Docker page
"Self-hosting" was misleading and put it above the Rune it was built for.
Reorder "As a Rune" above the Docker page, rename that page "With Docker",
and make the landing hero's primary call to action the Rune, with Docker
beside it.
Also drop the "the grammar is Todoist's" line from the filters page — the
factual import/export interoperability keeps naming Todoist, but the
positioning and soft comparisons do not.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claiming to be Todoist Pro is both inaccurate and an invitation to a
trademark complaint. Describe verdande on its own terms — a fast,
self-hosted task manager — and keep the Todoist references to what they
factually are: import/export interop, and a filter grammar close enough
that people coming from there already know it.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The landing page and the docs now show and describe what has landed since
the last site update:
- the delegated-task marker (an arrow to whoever has it) — visible now in
the board, list and Waiting-on-others shots
- snooze — one seeded task is parked so the board shows a card greyed at
the foot of its column
- Web Push reminders to iPhone and Mac, dated tasks in a subscribed
calendar, task durations on the calendar, and per-person note sharing
Screenshots regenerated against the current UI (PNG for docs/README, WebP
for the landing page, from the one capture). "Lately" brought current.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The assignee chip reads as a delegation now, so its title changed from
"Ansvarlig: {name}" to "Uddelegeret til {name}". Point the role-edit
smoke test at the new title.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A task assigned to somebody who is not you is out of your hands and into
theirs, but the row only showed their name — the same weight as any
other meta. The assignee is now a bordered chip led by an outgoing arrow
in the accent colour, so a list reads at a glance as which tasks are
yours to do and which you are waiting on. Covers the board too: its
cards render a TaskRow.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
On a phone the detail drawer's field grid stayed two columns, so each
field got half the viewport. Prioritet collapsed to "Ing" and the due
row — a date input plus fixed-width time and duration fields — ran off
the right edge. Below 560px the grid now stacks to one column per field
and the due row wraps.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
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 <[email protected]>
The calendar feed emitted every task as a VTODO and, on top of that, an event only
for tasks that carried a clock time. But a subscribed calendar in Apple Calendar or
Google ignores VTODO entirely and draws only events — so a task with just a due
date, which is most of them, appeared in the verdande app's own calendar and
nowhere in the calendar it had been subscribed into. "Verdande does not update in my
calendar, only in the verdande app."
Every dated, unfinished task is an event now: a clock time makes a timed event as
before, a bare date an all-day one (a DATE start and an exclusive next-day end, so
it stays on its own day). Finished tasks stay off the calendar, as they were.
Test updated: the sample's timed task and its two date-only tasks all become events
now, the all-day ones in the DATE form, and the finished one still makes none.
Co-Authored-By: Claude Opus 5 <[email protected]>
Every Web Push to an iPhone or a Mac was refused, silently as far as the person
waiting for the reminder could tell. The cause was the VAPID "sub" claim: on an
instance with no mail configured it fell back to mailto:[email protected],
and Apple answers 403 Forbidden to a mailto whose domain is made up. Verified
against web.push.apple.com on the live subscriptions — that mailto gets a 403, the
instance's https URL a 201.
So the subject is now a real mailto when SMTP.From is a real address, and otherwise
the instance's own https base URL — which is a valid VAPID subject, points at
something that exists, and every push service including Apple accepts.
Two things found alongside it, both fixed:
- A successful push did not clear the failure count. A subscription drops at ten
failures, and all four live ones sat at nine from this bug — one flaky push from
being deleted despite working. MarkPushDelivered zeroes the count and moves
last_used_at on every delivery, so a subscription that recovers is not still
carrying its old failures toward the cliff.
- A reminder with no project made a notification whose URL was "/projekt/" — a
dead link. Empty project now points at the app root.
Test: the subject is a real mailto when there is one, and the https URL when there
is not — including for the localhost default, which is not a real address.
Co-Authored-By: Claude Opus 5 <[email protected]>
Archiving a single note meant finding its row in the list and its hover-only
button. With a note open to read or write, the one place you would reach for it —
its own footer, beside Gem and Slet — had no way to put it away.
There is one now: Arkivér between Save and Delete, which reads "bring it back" once
the note is already archived. Owner only, like delete, and it runs the same archive
the row button does — the note leaves the list without leaving the account.
e2e: archiving from the footer takes the note off the list, the archive holds it
with a bring-back button, and bringing it back returns it. The existing bulk-archive
test is scoped to the selection bar, now that "Arkivér" also lives in the footer.
Co-Authored-By: Claude Opus 5 <[email protected]>
Choosing a note from the [[ completion wrote [[Title]] as plain text. It made a
real link behind the scenes — it showed in the panel under the note — but in the
note itself it stayed brackets, and "it doesn't become a link" is exactly how that
reads. A wiki link is a link where it is written.
Now a [[note]] renders inline as the title, in the accent colour, and a click on it
opens the note it points at. It is atomic — contenteditable="false" — so it moves
and deletes as one thing and a click is a click, not a caret landing mid-word. And
it appears the moment it is chosen: the completion drops the finished link in,
brackets and all swallowed, rather than text waiting to become a link when the note
is next opened.
The seam holds both ways: markdownToHtml lifts [[title]] into the link the same way
it already lifts an image, and htmlToMarkdown reads the link back to [[title]] by
the title it carries — so what is on disk is still the same Markdown, and a note
opened, saved and reopened is unchanged.
e2e: choosing a note writes an inline link, it survives the round-trip through
Markdown and a reload, and clicking it opens the note it names. Full suite green.
Co-Authored-By: Claude Opus 5 <[email protected]>
Picking a note from the [[ completion wrote [[Title]] and the server made a real
link from it — but the link only ever showed in the panel under the note as plain
text. It said the note pointed somewhere and gave no way to go there, which reads
as "it didn't become a link at all".
The links in that panel are buttons now. A [[note]] resolves by its title to the
note — the one already on the list, or looked up if not — and opens it in place; a
#project goes to its page; a task link to the task. Titles with punctuation work,
brackets and parentheses included, because the match is on the whole title, not a
parse of it.
e2e: after linking a note, clicking its chip opens the note it names.
Co-Authored-By: Claude Opus 5 <[email protected]>
AllNotes returns newest-first by the second, so two notes written in the same
second have no order between them — and the notes export walks that list assigning
filenames, giving the first note of a name the plain filename and the next the
"(2)". With the order undecided, which of two same-titled notes got which name was
a coin toss that landed differently on each export, and the export test failed
roughly one run in three.
Sort the export by when each note was made, with the id to break a same-second
tie. The first note of a name keeps the name; the archive is the same twice
running. Ids are time-ordered, so the tie-break agrees with creation order rather
than fighting it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Typing [[ did nothing. The toolbar hint offered "[[ another note" and the parser
turned a fully-typed [[Title]] into a link on save, but the completion that makes
it usable — the one # has for projects — was never there, so you had to know and
type a note's title exactly, brackets and all.
Now [[ opens the same picker # does, over the notes the page has loaded: the
current note is left out (a note linking to itself is a loop nobody meant), and
one without a title yet cannot be pointed at. Choosing a note closes its own
brackets — the opener is already in the text — and leaves a space, so the next
word is a word and not more of the tag. On save the link is real and shows in the
note's links, not merely as text.
The picker's two kinds are told apart where a project wears its colour: a note
suggestion wears the brackets that open it.
e2e: [[ offers a note by title, choosing it writes [[Title]] into the note, and
the link lands in the note's links. Full Chromium suite green.
Co-Authored-By: Claude Opus 5 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>