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>
335 lines
17 KiB
Markdown
335 lines
17 KiB
Markdown
<h1 align="center">verdande</h1>
|
|
|
|
<p align="center">
|
|
Self-hosted tasks and projects, shared with the people you share the work with.<br>
|
|
One binary, SQLite inside it, deployed as a Rune in Yggdrasil Panel.
|
|
</p>
|
|
|
|
<p align="center">
|
|
<img src="docs/screenshots/today.png" alt="The Today view: projects, filters and labels in the sidebar, beside the tasks due today." width="900">
|
|
</p>
|
|
|
|
---
|
|
|
|
**Verdande** is one of the three Norns — the one who spins *that which is becoming*.
|
|
Not what has been, and not what is fated to be: the thing you are in the middle of.
|
|
Which is what a to-do list is.
|
|
|
|
## Two editions: verdande and urd
|
|
|
|
This repository builds one program with two faces. Start it with
|
|
`VERDANDE_EDITION=notes` and it is **urd** — notes and nothing else: the task
|
|
routes are not mounted, the sidebar has no way to them, and the program calls
|
|
itself urd in its tab, its wordmark and its web manifest.
|
|
|
|
Urðr is Verðandi's sister in Völuspá, and the poem has the Norns cutting marks into
|
|
wood. Verdande is what is becoming; urd is what has become and was kept.
|
|
|
|
Not a fork, because the two share everything that is hard — accounts, sharing,
|
|
passkeys, the realtime socket, search, backups, migrations — and notes are about a
|
|
twentieth of the Go here. A test walks both routers and fails in either direction,
|
|
so "the task routes are not there" is measured rather than promised — 63 routes and
|
|
five of nine MCP tools. See
|
|
[Two editions](docs/editions.md).
|
|
|
|
## Status
|
|
|
|
All six phases are built. See [What works today](#what-works-today) for what is
|
|
tested and what is not.
|
|
|
|
Documentation: **<https://kristianwind.github.io/verdande/>** — the source is in
|
|
[`docs/`](docs/), built with Material for MkDocs and published from `main`.
|
|
|
|
## Why this exists
|
|
|
|
A fast, self-hosted task manager — projects, quick add, filters, notes and shared
|
|
lists — on your own hardware, with your own data, and with the integrations that a
|
|
hosted product will not build for you: a real CalDAV server, a mail-to-task address,
|
|
and an MCP endpoint so Claude can work your task list directly.
|
|
|
|
## What it does
|
|
|
|
- **Quick add that reads what you type.** `betal moms i morgen kl 10 p1 #Firma
|
|
@regnskab` becomes a task called "betal moms", due tomorrow at 10:00, priority 1,
|
|
in Firma, labelled regnskab. Danish and English, mixed freely in one line.
|
|
- **Projects, sections, sub-tasks, labels, saved filters.** List, board and calendar
|
|
views, and foldable groups over the projects in the sidebar.
|
|
- **Notes, beside the work.** Rich text that is Markdown underneath — headings,
|
|
lists, quotes, fenced code. A note is shared by filing it in a project, by
|
|
handing it to one person — including somebody who has no account yet, who gets an
|
|
invitation that becomes the share when they sign up — or by writing `@Sofie` in
|
|
the text, which shares it with her and tells her. `#project` and
|
|
`[[another note]]` are links, and a task shows the notes that mention it. A zip of Markdown goes in — Obsidian, Bear, or Apple Notes through
|
|
the exporter in [`tools/`](tools/) — and comes back out the same shape.
|
|
- **Notes are sealed at rest.** Title and body are encrypted in the database under
|
|
a key in a file beside it — the title too, because a title beside a sealed body
|
|
leaves the beginning of every note in the open. A copy of the file is not a copy
|
|
of the notes.
|
|
- **Drag where it means something.** Reorder a list or a board, file a project under
|
|
a group, drop a task on another project, or drop it on another day.
|
|
- **Sharing.** Owner, editor and viewer roles. There is no open registration:
|
|
everybody arrives through an invite link — to a project, to a single note, or
|
|
from **Settings → Brugere** to the instance itself. Nobody's password is ever
|
|
chosen by somebody else.
|
|
- **Live sync.** A change made by somebody else appears without a refresh.
|
|
- **Notifications that are worth reading.** A bell for what other people did to what
|
|
you share — folded, so one person typing for five minutes is one line — pushed to
|
|
your devices and counted on the app's icon in the Dock.
|
|
- **AI that suggests and never writes.** Optional, off by default, your own key and
|
|
your own model if you want. It tidies the inbox, pulls the tasks out of a meeting
|
|
note, writes a morning plan, turns *"SV: SV: Vedr. levering uge 12"* into the
|
|
action the mail asks for, and answers questions about your own notes with links to
|
|
what the answer rests on. Every one of them arrives as a line you accept, edit or
|
|
refuse — a model that edits your tasks directly has to be right every time to be
|
|
worth having.
|
|
- **Anything can put a task in the inbox.** A mail to your own forwarding address,
|
|
or a POST of one line of text from a shortcut on your phone, a script or any
|
|
service that can call a URL.
|
|
- **Recurring tasks** as RRULE (RFC 5545), reminders, comments and attachments.
|
|
- **Standards, not homegrown formats.** iCalendar and CalDAV, ICS feeds, OAuth2 with
|
|
PKCE, OpenAPI 3.1, MCP, Web Push, Argon2id.
|
|
- **A PWA that works on a phone**, installable, with swipe gestures and offline
|
|
reading of what it has already cached.
|
|
|
|
## What works today
|
|
|
|
**Every phase in the plan is built.** You can install it, add tasks by typing a
|
|
sentence, share projects, watch somebody else's changes appear live, set repeating
|
|
chores and reminders, save filters, import your Todoist account, sync two-way with
|
|
Apple Reminders, forward mail into it, and add it to Claude as a connector.
|
|
|
|
| Area | State |
|
|
|---|---|
|
|
| SQLite schema, migrations, WAL | Done, tested |
|
|
| Full-text search, Danish-aware | Done, tested |
|
|
| Quick-add parser (Danish + English) | Done, tested |
|
|
| Argon2id passwords, sessions, TOTP, recovery codes | Done, tested |
|
|
| Project roles and permission checks | Done, tested |
|
|
| REST API: projects, sections, tasks, sub-tasks, ordering | Done, tested |
|
|
| Today, Upcoming, search, activity log | Done, tested |
|
|
| Waiting on others: what you have delegated, by person | Done, tested |
|
|
| Assignee shown on every task that is somebody else's | Done, smoke-tested |
|
|
| Activity log, visible per project | Done, smoke-tested |
|
|
| Change a member's role without removing them | Done, tested |
|
|
| Sharing: invites, roles, members | Done, tested |
|
|
| Live sync over WebSocket | Done |
|
|
| Web interface: sign-in, views, quick add, ⌘K, shortcuts | Done |
|
|
| Task detail: description, sub-tasks, comments, files, reminders | Done |
|
|
| Settings: account, notifications, integrations, AI, tokens, data | Done |
|
|
| Device list: where you are signed in, and signing one out | Done, tested |
|
|
| User administration: invite to the instance, promote, remove | Done, tested |
|
|
| Server errors kept in the database, readable after a restart | Done, tested |
|
|
| PWA icons: PNG for launchers, apple-touch-icon for iOS | Done, smoke-tested |
|
|
| Invite and password-reset links that open the right page | Done, smoke-tested |
|
|
| Drag to reorder, in both the list and the board | Done |
|
|
| Drag a task to another project or another day | Done, smoke-tested |
|
|
| Foldable project groups in the sidebar | Done, tested |
|
|
| Month view on Upcoming, not only per project | Done, smoke-tested |
|
|
| Recurring tasks (RRULE, Danish + English) | Done, tested |
|
|
| Reminders, absolute and relative | Done, tested |
|
|
| Saved filters with a query language | Done, tested |
|
|
| Board and calendar views | Done |
|
|
| ICS calendar feed, task durations | Done, tested |
|
|
| Project templates | Done, tested |
|
|
| Nightly backups with rotation | Done, tested |
|
|
| Comments, attachments, notifications, Web Push | Built |
|
|
| Notes: rich text, Markdown on disk, links to projects, notes and tasks | Done, tested |
|
|
| Notes in and out: a zip of Markdown, both directions | Done, tested |
|
|
| Todoist CSV import and export, account export | Done, tested |
|
|
| MCP server for Claude | Done, tested |
|
|
| CalDAV server (two-way VTODO) | Done, tested |
|
|
| Mail-to-task | Done, tested |
|
|
| AI layer (Anthropic/OpenAI/Google/local) | Built, degrades when unconfigured |
|
|
| Gmail: OAuth2 with PKCE, polling to tasks, unstars what it imports | Done, tested |
|
|
| Google Calendar: read-only, laid over the task calendar | Built, never run against Google |
|
|
| Calendar subscriptions: ICS and webcal, several per person | Done, tested |
|
|
| Times on tasks, set by dragging in the week grid | Done, tested |
|
|
| Notes: archive, grouped list, images, source view | Done, tested |
|
|
| Update notice for administrators | Done, tested |
|
|
| Personal API tokens | Done, tested |
|
|
| OpenAPI 3.1 spec, checked against the router | Done, tested |
|
|
| End-to-end smoke tests (Playwright) | Done |
|
|
| Documentation, landing page, licence | Done |
|
|
| Notes sealed at rest: title and body encrypted in the database | Done, tested |
|
|
| `@name` in a note shares it with that person, and tells them | Done, tested |
|
|
| Share a project with somebody who already has an account, by name | Done, tested |
|
|
| Invitations that have not been taken up stay visible, and can be withdrawn | Done, tested |
|
|
| Told when a project is shared with you — and able to leave it | Done, tested |
|
|
| Push from other programs: a webhook address per person | Done, tested |
|
|
| Today's plan as a card on **I dag**, not only as a notification | Done, tested |
|
|
| Search the settings, from the page or from ⌘K | Done, tested |
|
|
| Choose which browser a link opens in | Done, tested |
|
|
| Images in notes: open full size, copy to the clipboard | Done, tested |
|
|
| Install count: two values, once a day, one click to stop | Done, tested |
|
|
|
|
**Not finished:** Web Push, the AI providers and the Google Calendar API calls are
|
|
implemented against their specifications and have only ever been exercised there,
|
|
not against live services. Gmail is no longer in that list — it runs in production,
|
|
including the flagging and de-duplication that took a day to get right. Calendar
|
|
subscriptions over ICS and webcal are proven; the Google half of the calendar is
|
|
not. Everything else has an interface: opening a task shows
|
|
its description, sub-tasks, comments, files and reminders, and the settings pages
|
|
cover the account, notifications, integrations, AI, API tokens, import, export and
|
|
templates.
|
|
|
|
Search finds `grøn` when you type `gron`, because Unicode does not consider `ø` an
|
|
accented `o` and a Danish-first app that cannot do this is broken for its own users.
|
|
|
|
## What it looks like
|
|
|
|
| | |
|
|
|---|---|
|
|
|  |  |
|
|
| Board view — one column per section. | The light theme. |
|
|
|  |  |
|
|
| A week at a time, so a task can be dragged across a month boundary. | A task, opened: description, priority, labels, sub-tasks and its files. |
|
|
|  |  |
|
|
| Notes, filed in a project to share them. | The same note as its source — which is what the export writes to disk. |
|
|
|  |  |
|
|
| Five themes and four looks: how bright, and how it reads. | What has been done on the server. |
|
|
|
|
Regenerate these with `node scripts/generate-screenshots.js` from `web/`. It starts
|
|
the real binary, creates its own account and a second one to share with, fills them
|
|
with demo data and photographs fourteen surfaces — nothing to set up and nothing to
|
|
remember. (`go run ./tools/shots` still works, and is the way to photograph an
|
|
instance that is already running.)
|
|
|
|
## Running it
|
|
|
|
### As a Rune in Yggdrasil Panel
|
|
|
|
verdande is built to be deployed as a **Rune** in
|
|
[Yggdrasil Panel](https://github.com/kristianwind/yggdrasil) — that is its home,
|
|
and the manifest ships in this repository. It runs anywhere Docker does as well.
|
|
|
|
1. **Runes → Carve a rune (upload)** and pick [`rune/verdande.yaml`](rune/verdande.yaml).
|
|
2. Create a server from it.
|
|
3. Set **Public address** to the address you will actually reach it on. verdande
|
|
cannot work this out for itself — the host port is not chosen until the server
|
|
exists — and invite links, password resets and calendar feeds are all built from
|
|
it. `{{PUBLIC_URL}}` is the default and is usually right.
|
|
4. Point the SMTP settings at your mail server. Leave the host blank and verdande
|
|
shows invite links in the app instead of emailing them, which is fine for a
|
|
single user and no use at all for sharing.
|
|
5. Start it, open the URL, create the first account. That account is the admin.
|
|
|
|
### With Docker directly
|
|
|
|
```bash
|
|
docker run -d --name verdande \
|
|
-p 8080:8080 \
|
|
-v verdande-data:/data \
|
|
-e VERDANDE_BASE_URL=https://todo.example.dk \
|
|
ghcr.io/kristianwind/verdande:latest
|
|
```
|
|
|
|
### From source
|
|
|
|
```bash
|
|
go run ./cmd/verdande
|
|
```
|
|
|
|
Listens on `:8080` and puts its data in `/data`, so for local work set
|
|
`VERDANDE_DATA_DIR` somewhere writable:
|
|
|
|
```bash
|
|
VERDANDE_DATA_DIR=./data VERDANDE_DEV=true go run ./cmd/verdande
|
|
```
|
|
|
|
A plain `go build` produces the API without the web interface — the frontend is
|
|
embedded only when built with `-tags embedweb`, so a clean checkout compiles
|
|
without a Node toolchain installed.
|
|
|
|
## Configuration
|
|
|
|
Everything is an environment variable; there is no config file. A Rune has nowhere
|
|
to put one that the operator would ever see.
|
|
|
|
| Variable | Default | What it does |
|
|
|---|---|---|
|
|
| `VERDANDE_BASE_URL` | `http://localhost:8080` | The public address. Invite links, password resets and ICS feeds are built from it. |
|
|
| `VERDANDE_ADDR` | `:8080` | Listen address. |
|
|
| `VERDANDE_DATA_DIR` | `/data` | Database, uploaded files and backups. |
|
|
| `VERDANDE_SESSION_TTL` | `720h` | How long a login lasts. |
|
|
| `VERDANDE_INVITE_TTL` | `168h` | How long an invite link stays valid. |
|
|
| `VERDANDE_RESET_TTL` | `1h` | How long a password-reset link stays valid. |
|
|
| `VERDANDE_SMTP_HOST` | — | Blank disables outbound mail; invite links are shown in the app instead. |
|
|
| `VERDANDE_SMTP_PORT` | `587` | |
|
|
| `VERDANDE_SMTP_USER` | — | |
|
|
| `VERDANDE_SMTP_PASS` | — | |
|
|
| `VERDANDE_SMTP_FROM` | `verdande@localhost` | Sender address. |
|
|
| `VERDANDE_SMTP_STARTTLS` | `true` unless port 465 | Port 465 is TLS from the first byte; everything else negotiates. |
|
|
| `VERDANDE_SMTP_INSECURE` | `false` | Skip certificate verification. For a self-signed internal mail server, and nothing else. |
|
|
| `VERDANDE_GMAIL_CLIENT_ID` | — | OAuth client from Google Cloud, if anyone will connect a mailbox. |
|
|
| `VERDANDE_GMAIL_CLIENT_SECRET` | — | |
|
|
| `VERDANDE_UPDATE_CHECK` | `false` | Ask GitHub whether a newer release exists. Sends nothing about your instance. |
|
|
| `VERDANDE_PANEL_URL` | — | The Yggdrasil panel this instance runs under, so it can ask to be restarted from its own settings. |
|
|
| `VERDANDE_PANEL_TOKEN` | — | An API token from that panel, belonging to somebody with control of this server. |
|
|
| `VERDANDE_PANEL_SERVER_ID` | — | Which server to restart — the id in the panel's URL. |
|
|
| `VERDANDE_DEV` | `false` | Human-readable logs at debug level. |
|
|
|
|
## What lives in `/data`
|
|
|
|
```
|
|
/data
|
|
├── verdande.db SQLite, WAL mode
|
|
├── files/ attachments
|
|
└── backups/ nightly snapshots, 14 days
|
|
```
|
|
|
|
Back up the whole directory. If you copy `verdande.db` on its own while the server
|
|
is running you will get a database missing its most recent writes — the `-wal` file
|
|
is part of it.
|
|
|
|
## Roadmap
|
|
|
|
1. **MVP** — auth, projects, sections, tasks, sub-tasks, priorities, due dates,
|
|
quick add, Today and Upcoming, search, keyboard shortcuts. *(done)*
|
|
2. **Sharing** — invites, roles, assignees, comments, attachments, activity log,
|
|
realtime sync, notifications. *(done)*
|
|
3. **Pro parity** — recurring tasks, reminders, filter queries, board and calendar
|
|
views, durations, ICS feeds, templates, automatic backups. *(done)*
|
|
4. **Import/export** — Todoist CSV and official export in, CSV/JSON/ICS out. *(done)*
|
|
5. **Integrations** — CalDAV server, mail-to-task, Gmail, MCP server for Claude,
|
|
and a provider-agnostic AI layer that also talks to a local model. *(done)*
|
|
6. **Publication** — documentation, landing page, open-source scaffolding. *(done)*
|
|
|
|
## Development
|
|
|
|
```bash
|
|
go test ./... # everything
|
|
go test -race ./... # what CI runs
|
|
gofmt -l . # must print nothing
|
|
```
|
|
|
|
Working on the interface means two processes: the API on 8080, and Vite on 5173
|
|
proxying `/api` to it, so the session cookie stays first-party exactly as it is in
|
|
production.
|
|
|
|
```bash
|
|
VERDANDE_DATA_DIR=./data VERDANDE_DEV=true go run ./cmd/verdande
|
|
```
|
|
|
|
```bash
|
|
cd web && npm install && npm run dev
|
|
```
|
|
|
|
To run the whole thing as one binary the way it ships:
|
|
|
|
```bash
|
|
cd web && npm run build && cd .. && cp -r web/build cmd/verdande/webbuild && go build -tags embedweb -o verdande ./cmd/verdande
|
|
```
|
|
|
|
Commits follow [Conventional Commits](https://www.conventionalcommits.org/);
|
|
releases are semver tags, which build and publish the image.
|
|
|
|
## Non-goals
|
|
|
|
Karma and points. Team workspaces and organisations. Native apps. Mail integrations
|
|
beyond Gmail and the forwarding address. Languages beyond Danish and English. A sync
|
|
protocol of its own — CalDAV and the REST API *are* the sync.
|
|
|
|
## License
|
|
|
|
[MIT](LICENSE). See [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md).
|