# Changelog All notable changes to OpenTodo are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] ## [0.15.0] - 2026-10-27 OpenTodo 0.15.0 moves the project to its own home, the **OpenTodoApp** organization on GitHub, with the image now at `ghcr.io/opentodoapp/opentodo`. The **Android app signs in behind a login page** (Pangolin, Authelia, Authentik, Cloudflare Access) and with single sign-on, inside the app, and targets Android 16 as Google Play requires. `ot vault` now also syncs **TaskNotes** task notes, the task panel shows a task's properties first and its notes in full, and a round of fixes for project counts, drag highlights, tab rows and task order. ### Upgrade notes - **Replace the image or the binary and restart.** No migrations. Upgrade the server before the devices, as for every release. - **New image name:** `ghcr.io/opentodoapp/opentodo`. The old name `ghcr.io/pixari/opentodo` still receives every release through 0.16.x, so nothing breaks; switch your compose file or template when convenient. ([Docker](docs/self-hosting/docker.md#the-move-to-opentodoapp)) - **Android app behind a login page:** see the new section in the [Android app guide](docs/android-app.md) for what works and which paths a bypass needs if you prefer one. ### Added - **`ot vault` syncs TaskNotes task notes.** Link the folder where the Obsidian plugin TaskNotes keeps your task notes with `ot vault tasknotes `, and every task note becomes an OpenTodo task, so it shows in Today, the calendar and your reminders. Status, priority, scheduled and due dates, tags and contexts, the project link, the estimate, recurrence with completed instances, time entries and the note's text sync both ways with the vault's field-by-field merge. The vault reads your TaskNotes settings, so renamed fields, your own statuses and priorities work. Only the keys whose value changed are rewritten, and the note is matched by an `opentodo-id` key TaskNotes leaves alone. ([Markdown vault](docs/vault.md#tasknotes), #440) ### Changed - **OpenTodo moved to the OpenTodoApp organization, and the image to `ghcr.io/opentodoapp/opentodo`.** The repository is now [github.com/OpenTodoApp/OpenTodo](https://github.com/OpenTodoApp/OpenTodo) (old links redirect), the image `ghcr.io/opentodoapp/opentodo` with the same tags, and the Helm chart `oci://ghcr.io/opentodoapp/charts/opentodo`. The old image `ghcr.io/pixari/opentodo` and chart `oci://ghcr.io/pixari/charts/opentodo` keep getting every release through 0.16.x and stop being updated with 0.17.0: replace `ghcr.io/pixari/opentodo` with `ghcr.io/opentodoapp/opentodo` where you run OpenTodo, nothing else changes. The Go module is now `github.com/OpenTodoApp/opentodo` (`go install github.com/OpenTodoApp/opentodo/cmd/ot@latest`). ([Docker](docs/self-hosting/docker.md#the-move-to-opentodoapp)) - **The task panel shows a task's properties first and its notes in full below them, without Show all.** Long notes no longer push the project, dates, priority and labels out of view: the panel lists them, the subtasks and the attachments first, then the whole note, which grows with the panel's scrolling. On a task page narrower than 1024 px the facts card now sits above the notes too. ([#444](https://github.com/OpenTodoApp/OpenTodo/issues/444), [The task page](docs/task-panel.md)) - **Plainer words in the start guide and How you work.** The start guide now says what Open Loops is before asking how much of it you want. The three choices are now called **Simple lists**, **Lists with method hints** and **Full Open Loops method** (they were Lists only, Companion and Open Loops first), the link reads **What is Open Loops?**, and the offers explain what a context and the Loop home are. Your choice is kept; nothing else changes. ([#433](https://github.com/OpenTodoApp/OpenTodo/issues/433), [Set up in ten minutes](docs/method/set-up.md)) - **The Android app targets Android 16 (API level 36),** as Google Play now requires. Nothing changes for users: back navigation already used the system's predictive back, and the app already drew edge to edge. ### Fixed - **The Android app signs in behind a login page and with single sign-on** ([#471](https://github.com/OpenTodoApp/OpenTodo/issues/471)). With the server behind a login wall (Pangolin with Pocket ID, Authelia, Authentik, Cloudflare Access, oauth2-proxy…), or with **Sign in with …**, the sign-in page opened in the phone's browser, so the app never got the session. The sign-in now happens inside the app, with a bar naming the site you are signing in on and a way back to your server; other links still open in the browser. The first-run screen no longer calls such a server "not an OpenTodo server": it says the server is behind a login page and offers to sign in there. Providers that refuse app web views, such as Google, still can't be used in the app; sign in with a password or passkey there. ([The Android app](docs/android-app.md#behind-a-login-page-pangolin-authelia-and-others)) - **Deleting or archiving a project names the right number of open tasks.** Opened right after the project, before its tasks had loaded, the confirmation could say "0 open tasks". The number is now counted when the confirmation opens. - **The drop highlight no longer stays on a shared project you can only view.** Dragging a task over such a project in the sidebar left its ring on after the drag ended. - **Tab rows no longer show a stray vertical scrollbar.** The task panel's Comments, Activity, History and Time row, and every other tab row, drew a small scrollbar at its right end because the selected tab's underline overflowed the row by one pixel. Tab rows now scroll only sideways. ([#445](https://github.com/OpenTodoApp/OpenTodo/issues/445)) - **The side panel no longer flickers when you pick the first task after changing lists** ([#442](https://github.com/OpenTodoApp/OpenTodo/issues/442)). After going to another project, the panel showing "No task selected" vanished for a moment and slid in again when you chose a task; it now stays in place and the task simply replaces the empty pane. - **Tasks added at the same moment keep their order.** Tasks created in the same instant could share one position and then show in any order in a manually ordered list; each now goes after the one before it. ## [0.14.2] - 2026-10-26 OpenTodo 0.14.2 is the second **security release** of the internal audit; please upgrade. It narrows what the server can reach on its own network (map proxy, private-target switch), keeps one client from using up a hosted tenant's or the demo's limits, tightens single sign-on linking and password reset, and closes attachment quota and cache gaps. **The first-run setup code is now off by default**: a fresh server can be set up without reading its log; set `OPENTODO_SETUP_CODE=required` to keep the code. ### Upgrade notes - **The first-run setup code is now off by default.** If you relied on it to protect a fresh server reachable from the internet, set `OPENTODO_SETUP_CODE=required`; `OPENTODO_SETUP_TOKEN` works as before. Without either, the first person to open a server with no owner becomes its owner: finish setup right after starting it. ([The setup code](docs/self-hosting/docker.md#the-setup-code)) ### Changed - **The setup code for a new server is opt-in.** A fresh server no longer asks for the one-time code from its log: the first person to open it creates the owner account, and the log says so at every start until then. Set the new optional `OPENTODO_SETUP_CODE=required` to ask for the code again; `OPENTODO_SETUP_TOKEN` still works and wins. On a server reachable from the internet, finish setup right after starting it, or set `OPENTODO_SETUP_CODE=required`. ([The setup code](docs/self-hosting/docker.md#the-setup-code)) ### Security - **Hosted services: the map proxy no longer reaches the operator's network.** On a server that hosts several tenants, a tenant's owner could choose a map provider that made the server fetch addresses on the operator's private network or a cloud metadata service. The map proxy of a hosted tenant now reaches only public addresses. Found in an internal audit. ([Host mode](docs/self-hosting/host-mode.md)) - **The map provider can no longer point your server at your local network.** A self-hosted tile or search server on your network keeps working, but only the two hosts you typed may be local: tile, font and sprite addresses that a provider's style names, and redirects, follow the same rules as a webhook. Found in an internal audit. ([Maps](docs/self-hosting/maps.md)) - **"Allow private network targets" no longer opens cloud metadata and link-local addresses.** With the switch on, webhooks, notification channels, calendar subscriptions, issue connections and AI providers still reach your LAN and this machine, but never `169.254.0.0/16`, `fe80::/10`, `0.0.0.0/8` or a cloud metadata address. Webhook test sends and redeliveries are limited to 30 per account per hour. Found in an internal audit. ([Webhooks](docs/webhooks.md#private-network-protection)) - **Hosted services: one client can no longer lock a tenant out.** The request limit per minute applied to all of a tenant's traffic, signed in or not, so anyone could use it up and every user of that tenant got "too many requests". It now counts each client address separately. Found in an internal audit. ([Host mode](docs/self-hosting/host-mode.md#limits)) - **Public demo: the sandbox limit can no longer be held by one party.** When the demo is full, the oldest sandbox now makes room for a new visitor after 15 minutes even if it is being kept busy, new sandboxes per IPv6 site (`/48`) are limited together, and the demo creates at most twice its sandbox limit per hour. Found in an internal audit. ([Demo instance](docs/self-hosting/demo.md)) - **Single sign-on links to an existing account only when its address was confirmed.** An address nobody confirmed (set without outgoing email) could catch another person's first sign-on. Accounts whose address was confirmed through an emailed link, or that already signed on with the same address, still link at the first sign-on; others sign in with their password once and link the provider in Settings → Account → Security. Found in an internal audit. ([Single sign-on](docs/self-hosting/sso.md)) - **The setup code, when turned on, is asked for from every other machine.** With `OPENTODO_SETUP_CODE=required`, a browser on the local network no longer skips it: behind Docker's port publishing, a NAT or a rootless container, a visitor from the internet could look local. Only a browser on the server itself, connecting directly, skips it. Behind a reverse proxy, a forged leading `X-Forwarded-For` entry no longer becomes the client's address. Found in an internal audit. ([The setup code](docs/self-hosting/docker.md#the-setup-code)) - **"Forgot password?" no longer reveals which addresses have accounts by its response time.** The reset email is now sent after the answer. Found in an internal audit. - **The attachment total can no longer be exceeded.** Undoing the removal of a file is now checked against your total, and several uploads at once share it, so neither can store more than the owner allows. Found in an internal audit. ([Attachments](docs/attachments.md#limits)) - **Files no longer stay readable in the browser after signing out.** Attachments are now checked with the server each time they are shown, and signing out asks the browser to empty its cache for OpenTodo (on https), so the next person on a shared computer cannot open them. Found in an internal audit. - **Links from encrypted data open only as web links.** A task's source link and a link attachment of an encrypted account open only `http` and `https` addresses; anything else is shown without a link. Found in an internal audit. ### Fixed - **The Assistant page shows its title on a phone in every language.** On a narrow phone, the page's History and New chat buttons took the whole top bar in French, Spanish, Italian, German and Portuguese, leaving no room for the title. Below tablet width the two buttons now show their icons only, and their names stay available to screen readers. ## [0.14.1] - 2026-10-26 OpenTodo 0.14.1 is a **security release**; please upgrade. It fixes the findings of an internal security audit: a shared project could take over another person's private task if its id was known, a saved view or a crafted email or calendar could slow down or crash the server, and an old session could add a new way to sign in without signing in again. It also makes the sync clock follow newer edits, so a device whose clock runs behind no longer loses its own changes, and fixes a long task scrolling the whole page in the side panel. ### Upgrade notes - **Replace the image or the binary and restart.** Upgrade the server before the devices, as for every release. - **Scripts and API clients may need a different token:** changing the owner's webhook, maps, MCP and audit settings now needs an `admin` token, the email-to-task routes refuse `read` and `agent` tokens, and adding a passkey, two-factor authentication or a single sign-on link needs a recent sign-in. ([HTTP API](docs/api.md)) - **One migration** (`0048_caldav_resources_per_account`): the names calendar apps chose for tasks are now kept per account; each existing name is kept for every account that can see its task. No data is rewritten otherwise and no environment variable changes. Replace the image or the binary and restart; there are no down-migrations, so back up first. - **Ids that two accounts already share stay as they are.** Earlier versions could store one id in two places (through the problem fixed below, or when two accounts imported the same export file). The server now reads and writes the oldest copy each person can see, so nothing is lost and nothing needs repair. To check whether your database has such ids, run `SELECT entity, entity_id, group_concat(DISTINCT scope_id) FROM scope_fields WHERE entity IN ('project','task','section','field','comment','attachment','time_entry','rule') GROUP BY entity, entity_id HAVING COUNT(DISTINCT scope_id) > 1;` with `sqlite3` on a copy of `/opentodo.db`. ### Added - **n8n nodes.** A separate package, installed from npm as `n8n-nodes-opentodo` once it is released (Settings → Community nodes in a self-hosted n8n), adds an **OpenTodo** node that creates, reads, changes, completes and deletes tasks, with quick-add syntax if you like, and manages projects, labels and comments, also as a tool for n8n's AI Agent. An **OpenTodo Trigger** starts a workflow when tasks or projects change: it sets up a webhook in your account, checks every delivery's signature and removes the webhook when the workflow is turned off. It needs an API token and no change on the server. ([n8n](docs/n8n.md)) ### Security - **A shared project can no longer take over a private task.** Someone who shared a project with you and knew the id of one of your private tasks (ids appear in task pages, public links, exports and calendar apps) could create a task with that id in the shared project. Your later edits to the private task then reached that project's members, and they could delete it. Every task, project, section, field, comment, attachment, time entry and automation rule id now names one item on the server: such a write is refused like any other write you may not make, and it reveals nothing about your item. Found in an internal audit. ([Sync protocol](docs/sync.md)) - **Calendar apps of different accounts no longer share resource names.** One account's calendar app could rename or drop the name another account's app had given a task they both see, which made items vanish or appear twice in the other person's calendar. Found in an internal audit. ([CalDAV](docs/caldav.md)) - **A project can only join your own workspaces.** Anyone could mark one of their projects as part of a workspace they did not belong to, which stopped the workspace's owner from deleting it. Such a change is now refused, and values written before are ignored. Found in an internal audit. - **Adding a way to sign in needs a recent sign-in** (found in an internal audit). Adding a passkey, setting up an authenticator app or linking a single sign-on account now asks for your password or a code when you last signed in more than ten minutes ago, like creating an API token already did, and doing one of them no longer counts as a recent sign-in. Before, someone with access to a long-open session could add their own passkey, authenticator app or provider account and keep access after a password change. Scripts that enrol with an `admin` token are unaffected. ([Account security](docs/account-security.md#confirm-its-you)) - **A saved view can no longer take the server down.** A signed-in person, including a visitor of a public demo, could save a view with a specially crafted filter query that crashed the server while checking it, stopping it for everyone (on a hosted server, for every tenant). Filter queries are now limited to 4,096 characters and 64 levels of parentheses and `!`, in the app and on the server; a longer or deeper query is refused with a message like any other mistake in a query. Queries people write, and every query the filter builder makes, are far below both limits. Found in an internal audit. ([Filters](docs/filters.md)) - **Importing a Things database can no longer tie up the server.** A specially prepared file uploaded for a Things import could make the server compute values while reading it, keeping a processor busy or using a lot of memory for each preview. The importer now refuses a file whose Things tables compute their values (Things' own databases never do) and any value over 1 MB, before or while reading it; real Things databases import as before. Found in an internal audit. ([Import](docs/import.md#things-database-things-db)) - **An import can no longer reach another account's attached file.** Importing a ZIP export that named the id of a file attached in someone else's account could link that file into the importing account, letting it be downloaded there. An import now stores each file itself and refuses one whose id already belongs to a stored file the importing account does not have; the row is listed among the import's errors. Found in an internal audit. ([Import](docs/import.md)) - **Reading your email-to-task settings no longer creates an address.** Before, simply opening the settings (or any token, even a read-only one, reading them) created an active address that accepts mail from anyone. Now the address is created when you turn email to task on, and read-only and assistant tokens can neither see nor create it. Addresses that already exist keep working. ([Email to task](docs/email.md)) - **The owner's security settings need an admin token.** Changing whether webhooks may reach private networks, the map providers, the AI assistant (MCP) switches or audit log retention now takes a signed-in session or an `admin` API token; a `full` token is refused, as for the other security settings. ([API](docs/api.md#token-scopes)) - **Adding people to projects and workspaces by email is limited** to 30 attempts per account per hour, so the share dialog cannot be used to find out which accounts exist on the server or to push projects at many people, and each person added to a project is recorded in the audit log as `project.member_added`. ([Sharing](docs/api.md#sharing)) - **A change made on a device whose clock runs slightly behind no longer loses to the older value it replaces.** Devices, `ot vault` and the server now move their clock past every change they receive, so the next edit is always stamped after the value it overwrites. Before, an edit made shortly after receiving a value from a device or server whose clock was a few seconds ahead (too little for the clock correction to catch) could be silently undone by that older value. With end-to-end encryption this could leave a value, such as a task's origin written by the email inbox, in plaintext on the server, because the device's encrypted rewrite lost to it and was never retried. Found in an internal audit. ([Sync protocol](docs/sync.md#clocks)) - **An email can no longer stall email to task.** Every message in the mailbox is read before it is matched to an account, and the HTML of a specially written message (tags that never end, or links, marks and lists nested thousands deep) took time or memory that grew with the square of its size: a few hundred kilobytes kept the mail poller busy for minutes, a 10 MB message for hours. Reading HTML now takes time in proportion to its size, nesting deeper than 32 levels is flattened, and the text of a tag that never ends is kept as text. Found by fuzzing. ([Email to task](docs/email.md)) - **A calendar server can no longer tie up calendar sync or subscriptions.** A multistatus answer whose text came in many small pieces between comments, or a `DURATION` with a very long run of digits, took time that grew with the square of its size. Both are linear now, a duration over nine digits per unit or longer than a century is refused, and a huge one no longer wraps around to a negative length. The same applies to CalDAV requests from your own apps. Found by fuzzing. ([Calendar feeds](docs/calendar-feeds.md), [Calendar sync](docs/calendar-sync.md)) ### Fixed - **Names with accents or non-Latin letters are no longer cut in the middle of a character.** API token, passkey, app password and single sign-on names, sign-up names and the device names recorded for sessions are kept within their length limits by bytes; a limit falling inside a character such as "ë" or "日" stored half of it, which then showed as "�". They are now cut before that character, and the same applies to error messages from push services, mail servers and calendar feeds. Found in an internal audit. - **A database read that fails part way is no longer taken as complete.** The cleanup of notification channel secrets, at start and with the regular maintenance, could remove the tokens of channels that still exist when reading the channel list stopped early with an error; it now removes nothing then. A Things import that hits a read error stops with the error instead of importing part of the database, and the webhook and notification channel workers retry instead of sending a partial batch. Found in an internal audit. - **`ot vault` no longer changes tasks by rewriting a file.** In some files the vault wrote a line that read back differently, so the next sync sent a change to the server: a task's location, origin or custom field holding a bracket or a line break, a title that began like a field (`[note::`) or ended in a marker such as 📅, a recurrence directly followed by another field, unusual spaces between words, a top-level task indented by a space (prose after it became its note), a note line of only spaces, prose starting with `---`, and old Mac line ends. A stray `]` in unknown text is now written as `\]`. Each of these now reads back exactly as written. Found by fuzzing. ([Vault](docs/vault.md)) - **A long task in the side panel no longer scrolls the whole page.** With a long note, the window got a second scroll bar and the page moved instead of the panel, pushing the comment box out of view. Only the panel scrolls now. ([The task panel](docs/task-panel.md)) ## [0.14.0] - 2026-10-26 OpenTodo 0.14.0 brings your calendar and your tasks together. **Calendar sync** connects the calendar you already use, Google Calendar or any CalDAV service such as iCloud, Fastmail or Nextcloud: your dated tasks appear there as events, and moving or deleting an event changes the task. **Today can show the day's calendar events** above your tasks, so one page gives the whole day. Also: the Calendar remembers the planning list, and fixes for oversized subtask checkboxes on phones, a flicker when switching tasks in the side panel, and the task page's breadcrumb. ### Upgrade notes - **One migration** (`0047_calendar_sync`: connections, calendars and the links between tasks and events) and a secrets file, `/secrets/calendar-sync.json`, that backups leave out like the other secrets: after a restore, people reconnect their calendars. Replace the image or the binary and restart. Upgrade the server before the devices, as for every release. - **Calendar sync is off until the owner turns it on** in Settings → Integrations → Calendar sync. Google Calendar also needs the owner's own Google OAuth client; the optional `OPENTODO_GOOGLE_CLIENT_ID` and `OPENTODO_GOOGLE_CLIENT_SECRET` (or `OPENTODO_GOOGLE_CLIENT_SECRET_FILE`) set it from the environment. No variable is required. ([Calendar sync](docs/calendar-sync.md)) - **Calendar events in Today is a new user preference field.** Devices still on 0.13 ignore it until they update. ([Sync protocol](docs/sync.md)) ### Added - **Calendar sync, both ways.** Connect the calendar you already use, iCloud, Fastmail, Nextcloud, Synology, Radicale or any CalDAV service with an app password, or Google Calendar, and your dated tasks appear there as events in a calendar you choose. Drag an event to another time, stretch it, rename it or delete it, and the task follows; change the task and the event follows. When both changed, the newer change wins. Other calendars of the same account can be shown in Day and Week. The server owner turns it on under **Settings → Integrations → Calendar sync** and, for Google, sets up an OAuth client there or with the optional `OPENTODO_GOOGLE_CLIENT_ID` and `OPENTODO_GOOGLE_CLIENT_SECRET`. Outlook and Microsoft 365 are not supported yet. ([Calendar sync](docs/calendar-sync.md)) - **Calendar events in Today.** With a subscribed calendar, **Show calendar events** in Today's menu lists the day's appointments above your tasks: all-day events first, then timed ones in order with their time, calendar colour and location. Free events are an outline, finished ones are dimmed, and a click shows the details as in Day and Week. Off until you turn it on; the choice follows you to your other devices. ([Calendar events in Today](docs/calendar-events-in-today.md)) ### Changed - **The Calendar remembers the planning list.** Left open, **Plan from a list** opens again the next time you come to Day, Week, Month or Plan on a wide screen, until you close it. ([Place tasks on the calendar](docs/place-tasks-on-the-calendar.md)) ### Fixed - **Subtask checkboxes are the right size on phones and tablets.** On a touch screen the circle beside each subtask grew to the size of the finger target and looked huge. The circle now stays small and only the area you can tap is larger. - **The side panel no longer flickers when you switch tasks.** With tasks opening in a side panel, moving to another task emptied the panel for a moment while the next one loaded, so the list beside it widened and narrowed again. The panel now keeps the previous task until the next one is ready. ([The task panel](docs/task-panel.md)) - **The task page's breadcrumb leads back.** Opened from Today, Upcoming, a label or a view, a task's page now starts its breadcrumb with that list's name as a link, which returns to the list with the task selected, like Esc. Before, it started with the task's project, or with the list's name as plain text when the task had no project. ([The task page](docs/task-panel.md)) ## [0.13.0] - 2026-10-25 OpenTodo 0.13.0 is about how you look at your lists. **Open tasks in a side panel** beside every list instead of on a page, give rows a **Detailed** layout with the parent task, all labels and a priority you can read without colour, and **group and order projects and labels** like saved views. **Settings opens as short lists of pages**, the Calendar's **Plan from a list** takes a label or a saved view, and a block's top edge now moves its start. Also: the **Home Assistant integration and add-on**, **import into a chosen workspace**, and fixes for the planning list, Undo notices, task details and section order. ### Upgrade notes - **No migrations, no new environment variables.** Replace the image or the binary and restart. Upgrade the server before the devices, as for every release. - **Display options and row layouts are new fields on labels, views, project preferences and user preferences.** Devices still on 0.12 ignore them until they update, and show their lists as before. ([Sync protocol](docs/sync.md)) - **Settings pages have new addresses**, such as `/settings/integrations/webhooks`. Old addresses, bookmarks and anchors keep working, and the single sign-on return address is updated by itself. ([Settings](docs/settings.md)) ### Added - **Home Assistant integration.** A separate integration, installed from HACS, shows the Inbox, Today and your projects as Home Assistant to-do lists you can change (with quick-add syntax), counts Today, Earlier and the Inbox, puts dated tasks on a calendar, and fires `opentodo_task_*` events through a signed webhook it sets up itself; Assist can add to and tick off the lists. It needs a Full access token and no change on the server. ([Home Assistant](docs/home-assistant.md)) - **Home Assistant add-on.** On Home Assistant OS or a Supervised installation, add the repository `https://github.com/pixari/opentodo-homeassistant-addons`, install **OpenTodo** and open it on port 8080. Options for the base URL, a setup token, open sign-up, trusted proxies and the log level; the data is in the add-on's own folder and in Home Assistant's backups. Images for amd64 and aarch64; each OpenTodo release becomes an add-on update within a day. Home Assistant Container installs run the Docker image instead. ([Home Assistant add-on](docs/self-hosting/home-assistant-addon.md)) - **Import into a workspace.** The Import screen has a **Workspace** choice when you have more than one: it starts with the workspace you have open, everything the import creates goes there, projects and labels are matched by name only inside it, and the preview names it ("12 new projects into Work"). An OpenTodo export keeps its own workspaces unless you pick one. From the terminal, the new `ot import --workspace Work` runs an import and waits for it (`--dry-run` shows the preview). ([Import formats](docs/import.md#workspace), [#349](https://github.com/pixari/opentodo/issues/349)) - **Change a task's start from the top of its calendar block.** In Day and Week, drag the top edge of a block to start earlier or later while the end stays; the estimate follows, as one change with **Undo**. Alt+Shift+↑ / ↓ does the same from the keyboard. ([Place tasks on the calendar](docs/place-tasks-on-the-calendar.md), [#355](https://github.com/pixari/opentodo/issues/355)) - **Open tasks in a side panel.** Under **Settings → Appearance → Task details**, **Open tasks** can be **In a side panel** instead of **As a page**. From 1024 px wide, every list (Inbox, Today, Upcoming, projects, labels, views, Next, Waiting and the others) then keeps the task beside it with its notes, subtasks, attachments and comments. A click on a task, or moving to it with the keyboard, shows it there, and Enter on the task shown opens its page. The panel closes only with its **×** (or Esc inside it) and stays closed until you open a task again. Drag its left edge to resize it; each device remembers its width. The choice follows your account; phones keep the page, and the page stays the default. ([The task page](docs/task-panel.md#open-tasks-beside-the-list), [#365](https://github.com/pixari/opentodo/issues/365)) ### Changed - **Detailed rows, and priority you can tell without colour.** Rows can now be **Detailed**: the title, the parent task, then one line with the date and time, the priority as bars and P1 to P3, the deadline, start, project, estimate, assignee and as many labels as fit, then "+2". Pick it for every list in **Settings › Appearance › Task rows**, or for one view, project or label in its **Display options**. One-line rows keep their layout and now show the priority as bars after the title too. ([One-line and detailed rows](docs/row-layouts.md), [#367](https://github.com/pixari/opentodo/issues/367)) - **Group and order projects and labels.** A project's list and a label's page now have **Display options**, like saved views: group by section, due date, priority or label (a label page by project, due date or priority), or not at all, and order by manual order, priority, due date, creation date, deadline or title. Each project and label keeps its own choice on all your devices; in a shared project it is yours only. Dragging a task, or moving it with Alt+↑ / ↓, while another order is shown switches back to manual order. Saved views can now order by deadline too. ([Group and order a list](docs/group-and-order.md), [#368](https://github.com/pixari/opentodo/issues/368)) - **Shorter Settings pages.** Account, Appearance, General, Notifications, Capture, Integrations, People and Server now open as a short list of their pages, each row with its state in a word ("Webhooks · 2", "Calendar feeds · Off"), and each page has its own address, such as `/settings/integrations/webhooks`. The language, date and time and the calendar grid moved from General to a new section, **Language and region**. Settings search and the command palette open the page that holds a setting, scrolled to it and briefly highlighted. Old links, bookmarks and anchors still open the right page. ([Settings](docs/settings.md)) - **Plan from a label or a saved view.** In the Calendar, **Plan from a list** can now list the tasks of a label or a saved view as well as the Inbox, tasks without a date, earlier tasks or a project. Label this week's tasks "week" and plan from that label. Type in the picker to find the source; this device remembers it. ([Place tasks on the calendar](docs/place-tasks-on-the-calendar.md), [#369](https://github.com/pixari/opentodo/issues/369)) ### Fixed - **A link can be attached to a task that has no attachments yet.** The paperclip beside a task's notes and **Attach** in the task's menu went straight to the file picker, so a link could only be added once the task had a file. The paperclip now offers **Choose files…** and **Add a link…**, and the task's menu has **Add a link…** too. ([Attachments](docs/attachments.md), [#342](https://github.com/pixari/opentodo/issues/342)) - **The planning list stays open across the Calendar.** After **Plan from a list**, moving to the previous or next day, week or month, or switching between Day, Week, Month and Plan, closed the list. It now stays open until you close it or leave the Calendar, and Month offers it too. ([Place tasks on the calendar](docs/place-tasks-on-the-calendar.md), [#350](https://github.com/pixari/opentodo/issues/350)) - **Placing a task from the planning list works with the keyboard.** After s on a task in **Plan from a list**, focus stayed in the list, so Enter started the placement over at its first time and Esc closed the list. The calendar now takes the keys: the arrows move the task, Enter places it and focus moves to the next task in the list, Esc cancels and keeps the list open. ([Place tasks on the calendar](docs/place-tasks-on-the-calendar.md), [#352](https://github.com/pixari/opentodo/issues/352)) - **The notice after Undo leaves on its own.** After **Undo** in the notice at the bottom of the screen, the "Undone." notice could stay until the page was reloaded. It now leaves after five seconds, and every Undo notice has a close button and closes with Esc while it has focus. ([Bulk actions](docs/bulk-actions.md), [#348](https://github.com/pixari/opentodo/issues/348)) - **A task's details show its planned block.** After placing a task on the calendar, its details showed only the start time, under **Do**, next to a **Start** row that is a different thing (the date a task becomes available). **Do** now reads like the calendar block, for example `Today 12:00–13:30` for a 90-minute estimate. ([Place tasks on the calendar](docs/place-tasks-on-the-calendar.md), [#356](https://github.com/pixari/opentodo/issues/356)) - **A section in second place can move up.** In a project where every task has a section, as after an import, **Move up** on the second section was disabled as if it were already first, because the list assumed a "No section" group at the top. Move up and Move down now follow the sections themselves. ([Sections and boards](docs/sections-and-boards.md), [#359](https://github.com/pixari/opentodo/issues/359)) ## [0.12.0] - 2026-10-25 OpenTodo 0.12.0 builds on Precision. **Press → on a task to see its details under the row**, without leaving the list. **Mention a whole group** with `@Adults`, and **filter by workspace** with `workspace: Home`. The comment box gets buttons for mentions and formatting, `ot` gets **shell completion**, and **end-to-end encrypted accounts can use the assistant** from their own device with their own key, when the owner allows it. The briefing links each full day, has a switch in Settings, and the rest of the app now holds up at larger text sizes. ### Upgrade notes - **No migrations, no new environment variables.** Replace the image or the binary and restart. Upgrade the server before the devices, as for every release. - **The assistant for encrypted accounts is off until the owner turns it on** in Settings → Assistant. While it is on, the page's Content-Security-Policy lets the app connect to the provider addresses the owner allowed, for every account on the server; turn it off to keep the policy as before. ([Assistant on encrypted accounts](docs/assistant-encrypted.md)) - **Group mentions are stored as `group:` in the existing mention lists.** Devices still on 0.11 ignore those entries until they update; the server notifies the group either way. ([Sync protocol](docs/sync.md)) ### Added - **Mention and format from the comment box.** The comment box has an **@** button that starts a mention (people first, then the workspace's groups), and on a computer a **Formatting** button that shows a row of bold, italic, link, lists and more. Comments are still stored as Markdown, so the API, `ot`, MCP and exports see them as before. ([Comments and activity](docs/comments-and-activity.md#comment-on-a-task)) - **Shell completion for `ot`.** `ot completion bash`, `ot completion zsh` or `ot completion fish` prints a script; install it once and Tab completes commands, flags and fixed values, plus project, label and workspace names, open task ids with their titles, and `#project` and `@label` in `ot add`, read from your profile's server. Offline it still completes commands and flags and prints nothing into your prompt. ([Shell completion](docs/cli.md#shell-completion)) - **Mention a group.** In a comment or a task's notes, `@Adults` mentions a group of the task's workspace: the mention list offers the groups after the people, the group shows as a chip with a group icon, and everyone in the group who can see the task gets one notification naming the group. Nobody outside the project is told, and someone mentioned twice is notified once. ([Workspaces](docs/workspaces.md#mention-a-group)) - **Filter by workspace.** `workspace: Home` (or `workspace: "Side projects"`) matches the tasks of that workspace, and a filter that names a workspace looks at every workspace, so `(workspace: Home | workspace: Work) & due = this week` lists both from anywhere; the filter builder has a **Workspace** condition. ([Filters](docs/filters.md#workspaces)) - **The assistant on encrypted accounts, from your own device.** The server cannot read an end-to-end encrypted account, so it never runs the assistant for one. When the owner turns on **Let encrypted accounts use the assistant from their own device** (off by default), the panel sets the assistant up on this device instead: you choose a provider the owner allows (Anthropic, OpenAI, Google Gemini or a model server such as Ollama at an address the owner listed), a model and your own key, which stays in this browser and is deleted at sign-out. Your device reads its own decrypted tasks and calls the provider directly; the server sees no content and no key, and usage is not reported. Proposals, review, undo and skills work as before. The page's Content-Security-Policy lets the app reach exactly the providers the owner allowed. ([Assistant on encrypted accounts](docs/assistant-encrypted.md)) - **Expand a row in place.** Select a task and press →, choose **Show details** in the row's actions, or tap the arrow at the end of a row on a phone: the task's notes (with **Show more**), its dates, project, priority, estimate, labels, assignee and what it waits for, its subtasks and its comment and attachment counts open under the row, without leaving the list. Tick subtasks right there; ← or Esc closes the details again. One row at a time, in lists and views (not on boards). ([Expand a row](docs/expand-a-row.md)) ### Changed - **App store templates match what was submitted.** OpenTodo 0.11.0 is submitted to the Umbrel, TrueNAS, CasaOS, Dokploy, Easypanel, Cosmos and Portainer stores. The TrueNAS app's default Web UI port is now **30536** (30530 was taken by another app in the catalogue), and the Dokploy template names a release such as `0.11.0` instead of `stable`, as Dokploy's catalogue requires; to update a Dokploy service, change its image tag and redeploy. On Umbrel, add OpenTodo to Apple's Calendar or Reminders with the full CalDAV address, because automatic discovery stops at Umbrel's login. ([TrueNAS](docs/self-hosting/truenas.md), [Dokploy](docs/self-hosting/dokploy.md#updates-and-backups), [Umbrel](docs/self-hosting/umbrel.md#umbrel-login)) - **The briefing links each full day, and has a switch in Settings.** When Upcoming says "Friday and Monday are already full", Friday and Monday are now separate links, each opening Upcoming at that day, in every language. **Settings → General → Briefing** has a **Show the briefing** switch, the same choice as **Hide briefing** in the menu of Today and Upcoming, on all your devices. "2 tasks are late" still selects those tasks, ready for the selection bar; Esc clears them. ([The briefing](docs/briefing.md)) ### Fixed - **Inbox zero is one quiet line when planned tasks are below it.** With every task that has no project already scheduled, the Inbox showed the large Inbox zero picture above the "Planned, no project" group and its count; it now shows only the sentence there, and keeps the picture for an Inbox with nothing below it. ([Today and Upcoming](docs/today-and-upcoming.md)) - **The bulk label picker shows the right checkmarks.** Opened with L straight after ticking tasks, the label picker showed no checkmarks until it was closed, and none changed after a pick; it now marks the labels every ticked task has as soon as it has read them, and updates after each pick. - **Larger text sizes on the rest of the app.** With the browser's text size at 20 or 24 px, the date picker's month no longer runs off a phone, the Share dialog's email field gets its own line, Settings keeps your name readable beside your role, the command bar's mode buttons wrap instead of overlapping, Statistics shows its figures in one column on a phone, and the Inbox's **Process** on a phone keeps only its icon so the title fits. Display mode, dialogs and the wide task panel now switch layouts at the same points as the rest of the app. ## [0.11.0] - 2026-10-24 OpenTodo 0.11.0 is **Precision**, a new look for the whole app. Every task is one line with its date, project and labels in aligned columns, pages fill one quiet surface, and colour is kept for what needs you. **Today tells you in a sentence what stands out**, and each part of that sentence opens what it names. **Tasks open as a page**, the **Inbox keeps the selected task beside the list**, **Filters become Views** with display options, **All projects is a table** that says which project needs a look, and quick add and Ctrl/⌘+K share one palette. The assistant, Settings, boards, the timeline, the review screens and the phone layout follow the same look, and larger text sizes no longer break the layout. Also new: **delivery channels** (ntfy, Pushover, Gotify, a signed webhook), a **filter builder** that needs no query syntax, and a **Claude Code plugin** for your own server. ### Upgrade notes - **One migration, no new environment variables.** Replace the image or the binary and restart; migration 0046 (delivery channels) runs on start. There are no down-migrations, so back up first. Upgrade the server before the devices, as for every release. - **Hiding the briefing adds one additive sync field** (`briefing` in the user's preferences); an older server refuses it as an unknown field and the device keeps its choice. No migration. - **Delivery channels add one migration** (`0046_notification_channels`: `notification_channels`, `notification_deliveries`) and a secrets file, `/secrets/notification-channels.json`, that backups leave out like the other secrets: after a restore, people enter their channel tokens again. No new environment variable. Channels are on by default; the owner can turn them off in Settings → Notifications. ### Added - **OpenTodo in Claude Code.** A Claude Code plugin connects Claude to your own server's MCP endpoint: `/plugin marketplace add pixari/opentodo-claude`, then `/plugin install opentodo@opentodo`, and enter your server address and an agent token (kept in your system's credential store). Its skills teach Claude to read before changing, use ids rather than titles, show a plan before changing several tasks, pass your time zone and never follow instructions written inside tasks, and add `/opentodo:today` and `/opentodo:plan-week`. The same MCP skill ships with each release as `opentodo-mcp-skill.zip`. ([Use OpenTodo from Claude Code](docs/claude.md)) - **Build a filter without typing.** The Filter view and saved filters show the query as conditions that read in words (*Label is errand*, *Due on or before tomorrow*, *Priority is P1*), in groups headed **All of these** or **Any of these**, with **Not** on any condition or group and a live count of matching tasks. **+ Condition** picks what to filter on and its value with the app's own pickers. The query text stays under the builder and follows every change both ways, so saved filters still store that text; the **Builder** button folds it away, remembered per device. ([Build a filter without typing](docs/filters.md#build-a-filter-without-typing)) - **Delivery channels: ntfy, Pushover, Gotify and a webhook.** **Settings → Notifications → Delivery channels** sends your reminders, tasks assigned to you, mentions, tasks that are ready to start and task page replies to ntfy, Pushover, Gotify, or any address as a signed JSON message (Apprise and Home Assistant read it as is), up to five channels each with their own events and **Send test**. Messages say only what happened unless you include task titles, and never name a task with end-to-end encryption. Failed messages are retried for about a day; a channel that keeps failing turns itself off. (#271; [Delivery channels](docs/notification-channels.md)) ### Changed - **A new, quieter look: Precision, part 1 of 3.** The colours are a flatter, darker slate with emerald kept for state and the main action; the sidebar sits on the plain background instead of a tinted one. Lists and controls use 13 px text (notes and help stay at 15 px), rows are 36 px (44 px on touch, 32 px when compact), corners are tighter, and only menus, popovers and dialogs keep a shadow. Every colour pair still meets its contrast minimum in light, dark, high contrast and all eight accents. The layout and the rows follow in the next two parts. ([Design system](docs/design-system.md)) - **Precision, part 2 of 3: one surface, two kinds of header.** Pages fill one content surface beside the sidebar instead of a narrow column, so wide screens are used. Today and Upcoming keep a large title with the date beside it; every other page has a compact one-line header with its title, its details and its actions. Every header starts with a sidebar toggle (also `[`). The current page in the sidebar is marked with a quiet grey instead of green, and the sidebar's tooltips teach their shortcuts ("Go to Today · G then T"). ([Interface guide](docs/interface-guide.md)) - **Precision, part 3 of 3: one line per task.** On a wide list every task is one line: the title on the left, and its date, project, labels (or deadline, or sub-task progress), estimate and assignee in aligned columns on the right, so you can scan down a list. As the list narrows (for example with a task open beside it) the columns at the end give way, and a narrow list keeps the title with its details underneath. The repeat mark and the comment and attachment counts sit beside the title. Group headers become quiet filled bars in normal case, the selected and hovered rows use quiet grey surfaces, and a project that hides its completed tasks says how many at the end of the list, with **Show**. ([Interface guide](docs/interface-guide.md)) - **Precision for the board and the timeline.** Board columns are bands that run the full height of the page, every column ends with **Add a task**, and **Add a section** is a dashed column after the last one. A card shows its name on up to two lines and then one row with the date, estimate and subtask progress (each with its icon), the labels, and the assignee at the end. On the timeline, rows are as tall as the list's and sections are its group bars. Long bars carry the task's name, short ones say how long they are, and a task past its deadline or due day turns red and says how late it is ("2 days late"). Deadlines are diamonds, and today is a green line with **Today** above it. Week columns read as ranges, such as "20–26 Oct". ([Sections and boards](docs/sections-and-boards.md), [Project timeline](docs/project-timeline.md)) - **Precision: Today tells you what stands out.** Under the title, Today says in a sentence what matters: "**Three things matter today.** Renew the passport is 2 days late, Monday is already full, and Mia answered about Boiler." It names late tasks (in red), a day that is already full (today past your day length, or the first full day of the week, in amber) and replies waiting for you; when nothing stands out it says so with what is planned, and an empty Today reads **All done for today.** with how tomorrow starts. Upcoming's sentence counts the next seven days, names the full ones and says how tomorrow starts, and the Daily check opens with Today's. Your device works it out from your own tasks, offline, in all six languages; no AI writes it. ([The briefing](docs/briefing.md)) - **The briefing opens what it names, and can be hidden.** Each part of the sentence on Today and Upcoming is a quiet link: a task's name opens the task, "2 tasks are late" selects exactly those tasks in Today's list (ready for the selection bar), "Monday is already full" opens Upcoming at Monday, and replies open your notifications. **Hide briefing** in the menu of Today or Upcoming hides the sentence on all your devices, keeping the title and the date; **Show briefing** brings it back. On a narrow Projects table, where the reason beside a project's health is left out, the health word shows it in a tooltip, or under the word on a tap. ([The briefing](docs/briefing.md#open-what-it-names), [All projects](docs/all-projects.md)) - **Precision: quick add and the command bar as one palette.** On a computer, Ctrl/⌘+K opens a wider box near the top of the screen. Its results sit under quiet headings, each command shows its keys at the end of its row (*G then U*), the selected task is named in the field, and a strip at the bottom names the keys and the `+`, `>` and `?` characters. The mode buttons now show only on phones and touch screens. In Add mode, quick add lists what it understood as rows: **When**, **Project**, **Label**, **Priority** and **Estimate**, each with its value or how to set it. Click a row, or move to it with ↓ and press Tab, to edit it in the line. Shift+Enter adds the task and opens it. When the box was opened with q, Esc closes it. On a phone, the add sheet shows plain chips for what it understood, with buttons to start a date, priority, label, estimate or project, and a send button. The same line still gives the same task everywhere. ([Quick add](docs/quick-add.md#in-the-command-bar)) - **Precision on the phone.** Today and Upcoming keep their large title, the date and the sentence about your day on the page, under a bar that holds only the menu and the page's actions. Every task is one line: its title, and at its end its date, or else the deadline, the assignee's picture or the project's dot; a time comes before the title. The bar at the bottom is **Inbox**, **Today**, **Upcoming** and **Search**, the current one in white or black rather than green; the menu button at the top left opens the drawer that **More** used to open. A task's page lists its facts as one card: what is set, **Do** and **Deadline**, and **More properties** for the rest. ([Interface guide](docs/interface-guide.md)) - **Filters are now Views, with display options.** The sidebar group, the overview and the search box say **Views**; the page that builds a query is **New view** (still g f), and a view's header reads **Views › name**. Under it, one toolbar shows the view's conditions in words (*Label is waiting*) with **Filter**; the filter button opens the builder and the query in a panel under it, and the new **Display options** button sets the layout (**List** or **Board**, one column per group), **Group by**, **Order by**, whether completed tasks are listed, and which properties the rows show (date, project, labels, estimate, assignee, deadline, start, origin). Each choice is saved to the view and syncs. **Save as view** names the view in place, keeping the conditions and results in sight, and saves the display options with it. Views live at `/views/…`; old `/view/…` and `/filter` links still work. ([Saved views](docs/saved-views.md)) - **The assistant gets a page of its own.** **Assistant** in the sidebar (g then k, until now the Ask screen) is the conversation itself: it greets you by name, puts the message box in the middle and offers three skills to start from (**Plan my week**, **Triage my inbox**, **Turn an idea into a project**); once you ask, the answers fill a centred column and the box moves to the foot, with **History** and **New chat** at the top. The message box looks like the comment box on a task, with **Skills** and the context the question carries (**Context: Today**, **Context: 1 selected task**) under the text and a round Send button. Proposed changes come as one card: "3 of 4 selected", each change in words at the right, and **Apply 3 changes**. Shift+I and **Ask** on Today still open the assistant beside the page you are on, now in the same style. ([Ask, review and undo](docs/assistant-ask.md)) - **All projects as a table that says where to look.** The Projects page (g then Shift+P) lists your projects as a table, sub-projects still under their parent. Each project shows its health in one word: **Late** (a deadline has passed), **Overdue** (open tasks are past their due date), **Quiet** (nothing changed for a week) or **On track**. Each also shows its progress, open tasks, next due date (red when it has passed), the people it is shared with, and when it last changed. Health is worked out on your device from those plain rules, offline too, and every device shows the same word. Select a column's header to sort by it. Arrow keys and j / k move between projects, ← and → fold and unfold, and on a phone each project keeps its name and its health. Archived projects move from the bottom of the page to an **Archived** tab. ([All projects](docs/all-projects.md)) - **Precision: a flat Settings page.** From 1024 px Settings has the same compact header as other pages, with a breadcrumb (**Settings › Notifications**). Beside the page sits a narrow pane with the search field (press `/` to jump to it), the groups and one row per section, and the page starts with its title and a line saying what it holds. Sections are no longer cards: each is a small heading, its line, and rows between hairlines. On or off, working or failing now read as a small coloured dot and a word instead of a coloured pill; roles and token scopes keep their labels. Switches are smaller (32 × 18 px, still 44 px to tap on a touch screen), selects draw their own chevron, and controls in a settings row are 32 px tall. Phones keep the list as its own page. The README, the Claude plugin's CLI skill, `ot` and two server error messages now point to **Settings → Integrations → AI assistants** instead of the old **Settings → AI**. ([Interface guide](docs/interface-guide.md#settings)) - **New drawings for empty pages.** The six empty and finished states (Inbox zero, an empty Today and Upcoming, a new project, Done for today, the empty Trash) are redrawn as one family. Each sits inside the open loop of OpenTodo's mark, and one small accent leaves through its opening: a paper plane, the sun, a kite, a sprout's leaf, the moon, a drifting leaf. They stand out better in the dark theme and in every colour theme. ([Design system](docs/design-system.md#illustrations)) - **A click anywhere on a task row opens it.** The title still edits in place or opens the task, as **Settings → General → Click a task to** says; a click on the rest of the row (its padding, the gaps, the space at its end) now always opens the task, where it used to do nothing. The checkbox, the chevron, the links and the row's actions keep their own clicks, and a drag never opens anything. ([Tasks and projects](docs/tasks-and-projects.md)) - **Everything you can click shows the hand.** Buttons, menu items, tabs, sidebar entries, suggestions and task rows show the pointing hand with a mouse; disabled controls and draggable calendar blocks keep their own cursor. - **Empty lists without a second "Add a task".** An empty Inbox, Today, Upcoming, Anytime, Someday, project, label or place shows its sentence (and its link, such as **Open Upcoming**) but no button: the quick-add field right above it is the way to add, and q still focuses it. Empty board columns and calendar days keep their **Add a task**, which adds to that column or day. - **Planned tasks without a project wait at the end of the Inbox.** The line under the Inbox title ("3 planned tasks without a project"), which opened Filter, becomes a folded group at the end of the list, **Planned, no project**, with its count. Open it to see and act on those tasks in place; your device remembers whether it's open. They still don't count as Inbox tasks: the Inbox can reach zero with the group below it, and Process the Inbox leaves them out. **Open as filter** on the group's header lists them in Filter as before. ([Today and Upcoming](docs/today-and-upcoming.md#where-planned-tasks-go)) - **Settings → Workspaces, clearer.** The list of workspaces sits beside the open one, which is marked and named in a header with its member count; on a phone a workspace opens as its own page with Back. Its settings are in tabs: General, Members, Groups and Invites. Members show their avatars. A group opens to rename it, add people by searching their name and remove them, and deleting a group first names the projects that lose access through it. Invite links can be for an admin (owner only) and last 1 hour, 1 day, 7 days or 30 days. ([Workspaces](docs/workspaces.md#members-and-groups)) - **Tasks open as a page.** Opening a task now shows it full page instead of in a panel beside the list: the top says where it lives and where it sits in the list ("4 / 12"), j and k open the next and previous task, and Esc goes back to the same spot in the list with the task selected. The task's facts sit in a column beside the notes under Schedule, Details, Labels and Project, and one you have not set reads as what sets it ("Set a deadline"), so nothing hides behind an Add menu. Subtasks show their progress, and the comment box has a round Send button. Process the Inbox still opens a task beside its card; on a phone a task opens as a page with Back in the bar. Older links with `?task=` still work. ([The task page](docs/task-panel.md)) - **Precision: the Inbox keeps the task beside it.** On a screen at least 1024 px wide, the Inbox shows the selected task next to the list, ready to edit. Move with j and k, or click a row, and the details follow. Enter opens the task as its page, and Esc clears the selection. With nothing selected, the right side reads **No task selected** and says how to go on. Each Inbox row now says where its task came from and when, such as "From email · 3 d ago", "From example.org", "From a calendar app" or "Added by an AI assistant". That line replaces the source link at the end of the row; the task itself still has the link. **Process** is the Inbox's main button, also P, and the header says how many tasks are left to sort ("4 to sort"). Narrower screens keep the plain list. ([Process the Inbox](docs/process-the-inbox.md#the-inbox-beside-its-tasks)) - **Precision for the screens that were not redrawn.** Selected rows in the Trash, Labels, Updates, calendar lanes and the timeline, and the current item of the workspace list, time list and the sidebar's footer, are grey like a selected task row instead of green; the bar that says "3 selected" is grey too, and ticked rows keep their green. The weekly review's step header is no longer tinted green. On a phone, a Trash row keeps its name readable and puts **Restore** and **Delete permanently** under it, and the Updates feed lines up its project names. The week agenda shows one quiet "Nothing planned." line per empty day instead of seven green buttons, as Plan does. ### Fixed - **A larger text size no longer breaks the layout.** With the browser's text size above the default, the app could hide the sidebar while still laying out for it (between about 770 and 960 px wide), or open a task into a hidden pane beside the Inbox. The app now switches layouts at the same points as its stylesheet. On a phone with a large text size, All projects puts the health under the project name so the name stays readable, and the Inbox's **Process** keeps only its icon. - **Bulk labels pick the right way round.** Picking a label in the bulk label picker before its checkmarks appeared could add a label every ticked task already had; it now removes it, as the checkmark would have said. - **A project's Completed group reads its count right.** Screen readers heard "2 open tasks" for the two completed tasks in a project's Completed group; they now hear "2 completed tasks". - **Timeline notes stay clear.** "2 days late" and "1 day" after a short bar no longer run across the today line or off the end of the timeline. ([Project timeline](docs/project-timeline.md)) - **Close the day closes the day you started.** Opened at 23:59 and finished after midnight, it used to switch to the new day midway: the figures changed, "Tomorrow" moved tasks two days out and the evening was never marked closed. The day is now fixed when the screen opens and stored with the close (an additive `closed_day` field on the review row; older closes fall back to their time). Carry over takes the unfinished tasks one at a time with their whole title, adds **Done already** (d), has no checkbox that completes a task by accident, and is applied when you move on, so its step shows a check instead of a count. The last step is now **A note for tomorrow morning**, with a preview. ([Rhythms](docs/method/rhythms.md#close-the-day)) ## [0.10.1] - 2026-10-23 A documentation fix for 0.10.0: nothing changes in the app or the server, and there is no migration. ### Fixed - **The documentation's Workspaces card shows its picture.** The docs index linked a thumbnail for the Workspaces guide that 0.10.0 did not ship; the release now carries it, and the docs check refuses a card without its light and dark thumbnails. ([Workspaces](docs/workspaces.md)) ## [0.10.0] - 2026-10-23 OpenTodo 0.10.0 is a calmer app with room for more people. The interface was redrawn: one quiet task row, a new mark and sidebar, softer colours, motion that marks a completed task, and a phone layout with swipe actions and a task sheet. **Workspaces and groups** keep work and home apart and share a project with a team in one step. **Automation rules** let a task change itself, notes and comments share **one editor** with tables, images, mentions and **reactions**, and a **task page** hands one task to someone without an account. Tasks can have a **location** on a map, your account can live as a **Markdown vault** for Obsidian, the importer reads **six more apps**, and the owner can give the server its **own name and logo**. An early **Android app** is in the repository, not yet in the stores. ### Upgrade notes - **Six migrations, no mandatory environment variables.** Replace the image or the binary and restart; migrations 0040 to 0045 (relay devices, task pages, projects kept away from AI assistants, automation rules, profiles, workspaces) run on start. There are no down-migrations, so back up first. New optional settings: `OPENTODO_MAPS`, `OPENTODO_MAPS_TILES_URL` and `OPENTODO_MAPS_GEOCODER_URL` (maps), `OPENTODO_STT_URL` and `OPENTODO_STT_KEY_FILE` (dictation through your own speech service), and `OPENTODO_AUTOMATIONS` (automation rules). Upgrade the server before the devices, as for every release. - **Sample tasks and the start guide's checklist add two additive sync fields** (`sample` on tasks, `start_checklist` in the user's preferences); older servers refuse them as unknown fields and the devices keep their values, older apps show sample tasks as ordinary tasks. No migration. - **Workspaces add one migration** (`0045_workspaces`: `workspaces`, `workspace_members`, `workspace_invites`, `groups`, `group_members`, `project_grants`) and a column `project_members.via`, added at startup together with one default workspace per account; every existing member becomes a direct grant. No new variable. Back up first, as for every release. An older server ignores the new tables and column, but devices from this version write `workspace_id`, which an older server refuses, so downgrade the server only together with the web app. - **Reactions and mentions in notes are new sync fields** (`reactions.` on comments, `note_mentions` on tasks); both stay plaintext with end-to-end encryption. Upgrade the server before the devices: an older server refuses the new fields. No migration. - **Quick add reads numeric dates in your date format only (breaking).** The device-level choice "Numeric dates like 12/11" under **Settings → Capture** is gone: `12/11` is read day-month or month-day as your date format in **Settings → General → Date and time** says, and with System or ISO as your language says. A device that had fixed the other order reads such dates differently now; the stored choice is ignored and removed. ([Quick add](docs/quick-add.md)) - **Profiles add one migration** (`user_profiles`, `email_changes`); pictures are stored under `/avatars` and are part of backups. - **Calendar alarms become reminders.** Display alarms set in a CalDAV calendar app turn into OpenTodo reminders the next time the app saves the task, and OpenTodo reminders now appear in calendar apps as alarms. Nothing is converted in bulk; alarms the reminder model cannot express (sound, email, repeating) stay the app's own data. - **Task pages add one migration** (`task_pages`, `task_page_replies`, `task_page_actions`) and two additive comment fields (`guest_name`, `page_id`); older apps ignore them. Publishing is refused on a demo instance. - **Maps are on by default.** Task locations show OpenStreetMap maps and search addresses through your server, which fetches from OpenFreeMap (tiles) and Photon (search) with no account data. Set `OPENTODO_MAPS=off`, or switch off **Settings → Server → Maps**, to keep the server from making those requests. Tiles are cached under `/cache/maps` (at most 200 MB, not backed up). ([Maps](docs/self-hosting/maps.md)) ### Added - **Your own name and logo.** **Settings → Server → Branding** lets the owner name the server (up to 40 characters) and upload a square logo: the name replaces "OpenTodo" in the browser tab, the installed app's name, the sign-in screen and the sidebar, and the logo becomes the favicon, the touch icon, the installed app's icons and the sidebar mark, rendered by the server in every size from one PNG or JPEG. The sign-in screen then reads "Powered by OpenTodo". Routes `GET`/`PUT /api/v1/settings/branding`, `PUT`/`DELETE /api/v1/settings/branding/logo` and the public `/branding/logo-.png`; `auth/status` carries `branding`; changes are recorded as `settings.changed`. ([Branding](docs/self-hosting/branding.md), #234) - **Profile pictures from your sign-in provider.** With the owner's new switch **Use profile pictures from …** in **Settings → People → Single sign-on** (off by default), the picture your OpenID Connect provider names becomes your profile picture when you sign in or link the provider: cropped to a square, scaled to 256×256 and re-encoded, so no metadata is kept. A picture you upload yourself is never replaced, one you remove is not fetched again, and a picture that cannot be fetched changes nothing. Uploading a picture was already there, in **Settings → Account → Profile**. ([Single sign-on](docs/self-hosting/sso.md#profile-pictures-from-the-provider), #233) - **An interface audit for contributors.** `make ux-audit` opens every screen and the main overlays of the app at 360, 768 and 1280 px, light and dark, in English and German, and writes a contact sheet with the findings ranked: overflow, target size, focus rings, cut-off text, overlapping controls, layout shift, movement under reduced motion, the interface budget and how fast the Inbox shows from the local copy with the server stopped. CI runs it on every code change and uploads the report. A screen inventory keeps the router, the layout check and the workbench in step, and the workbench's accessibility gate now also checks the focus ring on the core screens. The rarely used screens are in the workbench too (sign-in and the other signed-out screens, the invitations, display mode, the public list, Import, Help, the capture screen, a location's page, the About settings and the project and voice dialogs), each with a visual baseline in both themes at phone and desktop width. - **Completing a task feels like it.** The check fills and pops, the title is struck through and the row folds away while the rows below move up, all within 400 ms; the completion is saved at once, so c on three tasks in a row never waits and the cursor moves on. A repeating task stays and rolls to its next date. Undo during the moment keeps the row in place. Rescheduling, dragging, bulk actions and Undo slide the rows that stay to their new places (up to 40 rows; changes from other devices never move the list). Dialogs fade and scale in from 640 px and leave faster, the phone sheet and the task panel slide, toasts slide up, and menus fade in and close at once. Phones and tablets tick on completion and on a long-press lift, with a **Haptic feedback** switch in **Settings → Appearance** that follows your account. With the system's reduced-motion setting only colour and opacity fade; nothing moves or scales. - **Profile: your picture, name and email.** **Settings → Account** opens with a Profile card. Change your name, add a picture (crop it in a circle with the mouse, touch or the keyboard; the browser turns it into a 256×256 JPEG and the server re-encodes it so no location or other metadata is kept), or change your sign-in email: with outgoing email a link to the new address confirms it and the old address gets a notice, without it the change applies at once after you confirm your password. Pictures show wherever initials did (account menu, comments, assignees, members, history) for people who share a project with you. Accounts that sign in only through single sign-on keep the provider's email. Routes `GET`/`PATCH /api/v1/me/profile`, `POST`/`DELETE /api/v1/me/email`, `POST /api/v1/me/email/confirm`, `PUT`/`DELETE /api/v1/me/avatar`, `GET /api/v1/users/{id}/avatar`; audit events `account.name_changed`, `account.email_change_requested`, `account.email_changed`, `account.avatar_changed`, never with a name, address or picture. The cropper (react-easy-crop 6.2.3, MIT) is a lazy chunk of 7.75 KB gzipped. ([API](docs/api.md#profile)) - **Dictation through your own speech service.** The owner can connect a speech-to-text service that speaks the OpenAI-compatible `/v1/audio/transcriptions` request, such as Speaches or whisper.cpp, in **Settings → Server → Speech to text** or with `OPENTODO_STT_URL`, `OPENTODO_STT_MODEL` and `OPENTODO_STT_KEY_FILE` (none required). People then choose **This server's speech service** in **Settings → Capture → Voice capture**, after a statement naming the service's host: each phrase is recorded in the browser, sent once through your server and returned as text, and neither keeps the audio or the text. 5 MB and 60 seconds a phrase, 30 a minute per person, requests only to the configured host. ([Speech to text](docs/self-hosting/speech-to-text.md)) - **Moments and onboarding.** The start guide offers **Start with three sample tasks** instead of the "Show me around" project: three tasks marked **Sample** that show the panel, a time with an estimate and a subtask, never counted in Statistics, the Loop pulse or the weekly digest, and removed in one tap with Undo (the panel's notice, the Inbox's menu or the checklist). The guide ends on **Your first day** (add a task, plan today, get it on another device), which **Start guide · n of 3 done** under **More** reopens until it is done or hidden. Closing the day ends on **Done for today**: the time, the day's figures and your line for tomorrow, with no streak or score. Ctrl/⌘+K opens one command bar with **Search**, **Add**, **Commands** and **Ask** (when the assistant is on), switched with +, > and ?; Search always ends with **Add "…" as a task** (Ctrl+Enter), and q opens Add mode on screens without a quick add. Quick add tints what it recognised in the line itself, five new line illustrations fill the empty Today, Upcoming, project and Trash, and the undo toast pauses while hovered or focused, puts Undo first and never covers the focused element. ([Your first task](docs/your-first-task.md), [Search](docs/search.md)) - **Workspaces and groups.** Keep work and home apart: each workspace has its own projects, labels, filters and people, and the sidebar's header is now the workspace switcher, with Inbox and Today counts per workspace and a dot when another one has updates (one workspace reads **OpenTodo · Personal**, as before). Switching is per device; links, notifications and search results from another workspace switch for you. **Settings → Workspaces** manages members (by email), roles, groups and single-use invite links (`/invite/w/…`, sent by email when outgoing mail is set up); in a workspace you created, a project is shared only with its members and groups, and sharing with a group gives everyone in it one role, kept up to date as the group changes. **Move to workspace…** on a project re-matches its labels by name with Undo. Additive `workspace_id` on projects, labels, views and project-less tasks; the directory reaches devices in every sync (three queries). Routes under `/api/v1/workspaces` and `/api/v1/projects/{id}/grants`, workspaces in the export and the own-format import, one Inbox calendar per workspace over CalDAV (names prefixed outside the default workspace), `workspace_id` in webhook payloads, `list_workspaces` and a `workspace` argument for AI assistants (MCP and the assistant), and `ot workspaces`, `--workspace`/`OPENTODO_WORKSPACE` and `ot project move` in the CLI. Limits: 20 workspaces per account, 200 members and 50 groups per workspace, 100 people per group, lower per tenant in host mode. End-to-end encrypted accounts can create workspaces and move things, but not add members, groups or links yet. ([Workspaces](docs/workspaces.md)) - **An Android app.** `mobile/android` is a small Kotlin app that opens your own server in a full-screen web view and adds what the installed web app cannot do on Android: reminders scheduled on the phone with exact alarms and no push service (refreshed hourly in the background, with **Snooze 10 min** and **Dismiss** that act like the web notifications), the share menu for links, text, photos and files (also on phones without Google Play services), a Today widget and a quick-add widget, and fingerprint or face unlock of an end-to-end encrypted account (the key is kept by Android's keystore and never leaves the phone). The first-run screen checks the address and says why it can't connect; plain `http://` works only on your own network. The web app talks to it through an optional, versioned bridge (`window.opentodoShell`, only on your server's origin): **Settings → Notifications** reads **Notifications through the app**, **Settings → About** names the app and warns when it is too old, and nothing changes in a browser, where web push stays as it is. AndroidX only, no trackers, release APK about 1 MB; `make android` builds and tests it, a new CI job runs on changes under `mobile/` and signs the APK and AAB on tags once the signing secrets exist. Not in the stores yet. The web bundle grows by about 4 KB gzipped. ([The Android app](docs/android-app.md)) - **Assistants plan with the app's own words.** MCP, `ot` and the assistant in the app can now set a task's deadline, start date, Someday, This Evening, what it waits for and its location: `create_tasks` and `update_tasks` take `deadline`, `start`, `evening`, `blocked_by` and `location`, task results carry them with a derived `blocked`, `list_tasks` gains the `someday` and `anytime` views and a `blocked` filter (and two resources), and the agent REST routes accept the same fields, so `PATCH {"start": "someday"}` with an `agent` token works (#198). `ot add`/`ot edit` gain `--deadline`, `--start`, `--evening`, `--blocked-by` and `--location`; `ot list` gains `--someday`, `--anytime`, `--blocked` and a `BY` column. `GET /api/v1/places` lists saved locations. The assistant's instructions and the Plan my week, Weekly review and Triage skills use the new fields, and its review describes them. ([AI assistants (MCP)](docs/mcp.md), [CLI](docs/cli.md)) - **Keep a project away from AI assistants.** The owner or an admin of a project can switch on **Keep away from AI assistants** in the share dialog or the command palette: no member's assistant (MCP, `ot` with an `agent` token, the assistant in the app) sees the project, its sub-projects or their tasks, and writes to them fail as not found. The app, sync, CalDAV, feeds and the export are unaffected. Routes `GET`/`PUT /api/v1/projects/{id}/agent-access`, `agent_excluded_projects` in `GET /api/v1/me`, audit event `project.agent_access`; one migration (`project_agent_exclusions`); no new dependency. ([Sharing](docs/sharing.md), [API](docs/api.md#keeping-a-project-away-from-assistants)) - **Browser extension 0.6: a popup and Quick save.** The toolbar button and Alt+Shift+O open a small popup with one quick-add line from the page, a project and a label picker, and the page as the origin. In the new **Quick save** mode, with a full-access API token kept on that device only, Save creates the task at once and previews the line as chips in your account's language; the default **Capture screen** mode opens the capture screen as before and sends nothing itself. "Add to OpenTodo" now works on links and images, and a message open in Gmail or Outlook on the web becomes a task with its subject, its sender and a **Gmail**/**Outlook** origin (`origin: gmail`). Two packages, one for Chromium and one for Firefox (now 128 or later). New route `POST /api/v1/quick-add/parse` answers how a line reads without saving anything. See [Capturing pages](docs/capture.md#browser-extension). - **Automation rules.** **More → Rules** lets a task change itself: *when the label waiting is added, move it to Waiting*, *every Sunday at 18:00, give overdue tasks tomorrow's date*, *when a task is due, notify me*. A rule has a trigger (created, completed, reopened, a label added or removed, assigned to me, moved, due, overdue, or a schedule), optional conditions (project with sub-projects, labels, priority, date, deadline, assignee, blocked and more, with a live count of matching tasks) and actions (priority, date, start, deadline, assignee, labels, project and section, complete or reopen, a comment, an in-app notification, a delivery to one webhook as the new `rule.fired` event). Rules sync as a new `rule` entity, are saved offline and run on the server for their author only, never for imports or a teammate's edits; each run is one batch per task with device `rule:`, shown as **Rule (name)** in the task's history and undone like any change. Limits: 60 runs a minute and 2,000 a day per account, 500 tasks a run, 50 rules; a rule that keeps hitting the limit is paused with a notification, rules that trigger each other stop after three in a row, and **Pause all rules** pauses yours. A rule's **History** lists its last 100 runs. Encrypted accounts can use every action except the comment. The owner can turn automations off in **Settings → Server → Automations** or with `OPENTODO_AUTOMATIONS=off`; a demo never runs them. Also `ot rules list|enable|disable|runs`, the read-only MCP resource `opentodo://rules`, rules in the export, and **Make an automation rule** on saved filters and board columns. One migration (`rule_events`, `rule_runs`, `rule_state`, `user_rules_paused`). ([Automation rules](docs/automations.md)) - **One editor for notes and comments, with tables, quotes, code, images and mentions.** Type `/` in the notes or a comment for a menu of everything the editor inserts: headings, lists, a table (Tab moves between cells; up to 10 columns and 100 rows), a quote, a code block, a link, an image, today's date or a mention. Paste a screenshot into the notes or a comment and it becomes an attachment shown in place (decrypted on your device with end-to-end encryption). `@` mentions a member in a task's notes, who is notified once. On a checklist line, Ctrl+Shift+Enter makes it a subtask linked from the note (Ctrl+Z takes it back), and GitHub, GitLab, Jira and Linear issue links read as chips. On a phone a row of formatting buttons sits above the text while you write. Comments stay Markdown: the API, the CLI and email see pipe tables and `![…](attachment:…)`, and a comment saved unchanged stays byte for byte the same. - **Reactions on comments.** Thumbs up, heart, check, eyes, party popper or question under any comment, offline too, with who reacted on hover; `PUT /api/v1/comments/{id}/reactions` sets yours from a script, and comments in the API and the export carry `reactions` and `my_reactions`. - **Estimates and reminders over CalDAV.** A task's duration estimate travels as `ESTIMATED-DURATION`, and reminders and calendar alarms are the same thing seen from two sides: reminders are served as `VALARM`s with a stable UID, and a display alarm added, changed or removed in a calendar app adds, changes or removes the reminder. A calendar app that never shows alarms cannot delete reminders by accident. ([Calendar apps](docs/caldav.md#alarms-and-reminders)) - **A Markdown vault for Obsidian and plain text.** `ot vault init ~/Notes/Tasks` writes your account as a folder of Markdown files, one per project (`Inbox.md`, `Home.md`, sub-projects in a folder next to the folder note `Work/Work.md`), each task a checklist line in the Obsidian Tasks format: `📅` do-on date, `🛫` start, `🔁` repeat rule (`when done` for after completion), `🔺 ⏫ 🔼` priority, `#tags` as labels, `⛔` blockers, `🆔` the task id, and Dataview fields such as `[deadline:: ]` and `[estimate:: 45m]`. `ot vault sync` (or `ot vault watch`, which follows the server's live channel and the files) keeps it in two-way sync: tick a line, add one, nest it, move it under a `## section`, rename or move a file, and every device sees the change, which history credits to "a Markdown vault". The vault is an ordinary sync device with a three-way merge per field: a field changed on both sides keeps the app's value and puts the file's line in `.conflict.md`, a removed line trashes its task unless the app changed it, a deleted file is written again, and prose, other headings and text such as `⏳` stay where you wrote them. `--dry-run` plans without writing, `--read-only` pulls without sending, `ot vault status` works offline. Limits: 1 MB and 5,000 task lines per file, 20,000 tasks, 64 KB per note, checked before anything is written or sent. On an end-to-end encrypted account the vault needs `ot encryption unlock`, decrypts and encrypts on your machine so the server sees only ciphertext, and says once that the folder holds your tasks in plain text. CLI only: the server binary is unchanged and `ot` grows by about 380 KB. ([Markdown vault](docs/vault.md)) - **Switch from six more apps.** The importer reads Google Tasks (Takeout's `Tasks.json`), Trello (a board's JSON, or a ZIP of boards), Asana (a project's CSV), Microsoft To Do, Apple Reminders and Any.do, detected from the file like the others. Microsoft To Do and Apple Reminders have no export, so their guides give a short PowerShell script (Microsoft Graph, read-only) and a Shortcut that write one JSON file; Apple Reminders can also sync over CalDAV. Any.do's file comes from its data request and is undocumented, so it may change. Lists become projects (the default list goes to your Inbox), Trello boards open as boards, checklists and steps become subtasks, and every task keeps its origin. Trello custom fields and Asana's extra columns become custom fields of the project (numbers, checkboxes, dates, choices or text), and **Linear estimates now become a number field "Estimate"** instead of being dropped; the preview counts new fields, and undo removes the ones the import created. A switch guide per app under Help. No new dependency; the Windows time zone table (generated from CLDR) adds about 8 KB to the binary. ([Import formats](docs/import.md)) - **Task pages: hand one task to someone without an account.** **Publish task page…** in the task panel's menu turns a task into a calm page at a private, unindexed link: its title, dates, description, checklist, links, map and conversation. Choose whether guests can read, reply, or reply and tick (subtasks, and the task itself, which they can reopen within a day); add a label, an expiry and the map. Guests type a name once; their replies arrive as comments by "Marco (guest)", history names them, and you get a notification. **Published** in the panel's header shows the page's status, a new link or **Revoke**. End-to-end encrypted tasks can be published too: your device encrypts the page with a key only the link carries after `#`, which the server never receives, and imports the guests' encrypted replies. The page is its own small document with no account, no cookie and no third-party request. ([Task pages](docs/task-pages.md)) - **Task locations.** A task can say where it happens: search an address, type coordinates, pick a point on the map or use your current position, from the task panel's **Where** line. A small map and **Open in maps** (your phone's maps app, or openstreetmap.org) show it. **Save for reuse** keeps a location under a name; **Locations** in the sidebar's **More** lists them with their open tasks and a map, and a task linked to one follows it when you rename or move it. Locations sync as one additive `task.location` value and a new `place` entity, work offline (except the address search), are in the export, and are encrypted for end-to-end encrypted accounts. Each user can turn maps off in **Settings → General → Maps**. The map code (MapLibre GL, about 280 KB gzipped plus its worker) loads only where a map is shown; the main bundle grows by about 2 KB. ([Locations](docs/locations-and-places.md)) - **Cloud AI connectors on an encrypted account, through your own device.** Turn on **Settings → Integrations → AI assistants → Answer AI connectors here** on an unlocked device, and a connector such as Claude.ai can read and write your encrypted tasks while that device is online: the server sends it, sealed for that device only, the values each call needs, and the device decrypts or encrypts them with a key that never leaves it. The server never stores, logs or audits what it relays, but the text a connector asks for passes through its memory, and Settings says so. The device decrypts at most 5,000 values an hour and 20,000 a day, lists recent requests without their text, shows a key in the sidebar while it answers, can ask before every change (**Confirm changes first**), and stops when you lock it or sign out. Off by default, refused on the demo. New audit events `relay.enabled` and `relay.disabled`; one migration (`relay_devices`); no new dependency. ([AI assistants (MCP)](docs/mcp.md#cloud-connectors-through-your-device)) - **Reopen the day.** Closed the day too early? Until your day ends, **Reopen the day** next to **Day closed** (or at the top of the Close the day screen) puts the carried-over tasks back where they were and clears the close, on any of your devices, offline too; the line for tomorrow is kept, a task you moved again by hand after closing stays where you put it and is named, and **Undo** takes the reopen back. The carry-over is recorded in an additive synced field, `review.carry_over` (task ids, fields and dates, never content). ([Rhythms](docs/method/rhythms.md#close-the-day)) - **Drop a task on the sidebar.** From any list or board, drag a task onto a project or **Inbox** to move it there, or onto a label to add that label; ticked tasks go along as one change with **Undo**, and projects you can only view don't accept drops. ([Change several tasks at once](docs/bulk-actions.md#drop-on-the-sidebar)) - **Hundreds of icons, with search.** A project's **Icon…** opens a picker with 411 icons in 16 categories instead of 24: type to search by name, keyword or category (category names in your language), use the arrows and Enter, or pick **No icon** for the coloured dot. The icons load the first time you open the picker and then work offline; an icon set on a newer device shows as the dot until this one updates. The app's main bundle grows by about 0.2 KB gzipped, and the catalogue is its own chunk of about 34 KB gzipped. ([Projects](docs/projects.md)) - **Section colour and icon.** A section's menu has **Colour…** and **Icon…**: the icon, in the section's colour, or a small dot of its colour appears before its name in the list and on the board. New sections have neither, and **No section** never does. They sync as two additive fields, `section.color` and `section.icon`, are not encrypted content (so encrypted accounts can use them), travel in the export, and are accepted by `/api/v1/sections`, which refuses an icon outside the catalogue or a colour that is not `#rrggbb` with `400 invalid_input`. ([Sections and boards](docs/sections-and-boards.md), [API](docs/api.md#sections)) - **A template library in the app.** **From a template…** (and **New project from a template** in the command palette) now opens on ten curated templates: trip planning, moving house, household chores, home maintenance, meal planning, a new job, a product launch, a study plan, a weekly review and a Getting Things Done setup. Search them by title, label or task, read what each one is for and preview every section and task with when it falls ("day 3", "week 2") before anything is created; ↓, Enter and Esc work throughout. The library works offline, is in Italian as well as English, and reuses labels you already have by name. **Upload a file…** in the same dialog keeps taking template files. ([Project templates](docs/templates.md)) - **Templates are contributed on GitHub.** The library lives in `templates/`, one folder per template in a short hand-written YAML format with optional translations, checked in CI (fields, declared labels, icons, size limits, no personal data) and started by a test on a fixed day. [`templates/README.md`](templates/README.md) has the format and the bar; templates are shared under CC BY 4.0. The site bundle carries the same library (`templates/index.json`, one file per template, a `templates` fact) for the website. The library is a separate chunk of about 12 KB gzipped, loaded when the dialog opens; no new dependency. - **Assistants can arrange projects and sections.** Over MCP an assistant can now recolour a project, give it an icon, rename it, move it under another project or reorder it (`update_project`), pin it to Favourites (`set_favourite`), and create, rename, restyle and reorder sections (`create_sections`, `update_sections`, `reorder_sections`). `list_icons` searches the icon catalogue the app offers, and every icon is checked against it. Archiving and deleting stay out of reach; the owner's and your switches, the limits and the audit apply as before. Creates take an optional `id` you choose. ([AI assistants](docs/mcp.md#write-tools)) - **A local assistant for encrypted accounts.** `ot encryption unlock` unwraps your account key with your encryption passphrase (or recovery key) on your computer and stores it owner-only; `ot mcp --decrypt` then lets an assistant such as Claude Desktop read and write your encrypted tasks, decrypting and encrypting on your machine while the server keeps receiving only ciphertext. The assistant's AI provider does receive the plaintext, and the bridge says so when it starts. Remote assistants of an encrypted account can now change colours, icons, order and Favourites; text still needs ciphertext. ([AI assistants](docs/mcp.md#encrypted-accounts-on-your-computer), [Command-line client](docs/cli.md#mcp-bridge-and-encryption)) ### Changed - **Overdue is now Earlier, and pages have a rhythm.** Today's first group is **Earlier**: a quiet label with only each task's date in red, and two actions always in its header, **Move all to today** and **Reschedule…**, which change the whole group in one batch with Undo (also in the palette as **Move earlier tasks to today** and **Reschedule earlier tasks…**). With more than five tasks it starts folded on a device that never opened it. The word "overdue" leaves the interface, the Daily check, the display, the weekly review, Close the day and the weekly digest ("Still open from earlier"); the filter keyword `overdue` is unchanged. Dates climb a ladder: later dates muted, tomorrow in the text colour, today in the accent, an earlier day in red. Page titles are larger, with the date and totals (or a project's members, a label's count) on a soft line under them. Group headers are small labels, uppercase for the groups the app makes and in your own case for sections and projects. Pages without a layout switch are centred in wide windows and slide to the new centre when the task panel docks; a project keeps one frame for list, board and timeline, and its list now uses the width. - **The app on a phone.** Two rows sit above your tasks: the top bar and quick add. A list's buttons (Process, Ask, the duration filter) moved into the top bar, and **Select tasks**, Display mode and a project's layout and Share into its **⋯** menu. The Inbox's "planned without a project" count is the bar's subtitle. Swipe a task right to complete it, left to schedule it (Today, Tomorrow, Next week, a day you pick, or Someday); the same two actions start the task's menu, and a one-time hint shows the gesture. Press and hold a task to select it; **Done** in the selection bar hides the checkboxes. A task opens as a sheet over its list, with a handle you drag down to close it. Menus and pickers close the same way. While you scroll down, the bottom bar shrinks to icons and the **+** slides away. The list keeps room for the **+** under its last task, and the Undo toast sits above both. **More** opens a wider drawer with what the bottom bar doesn't have, in larger rows. ([Work from the list](docs/work-from-the-list.md), [Bulk actions](docs/bulk-actions.md)) - **A new mark, and a quieter sidebar.** OpenTodo has its own mark, an open loop that closes into a tick, beside the name at the top of the sidebar and on the sign-in, setup, reset, invitation and email screens. The app icon, the favicon, the Apple touch icon and a new maskable icon for Android home screens use it too; installed apps pick it up on their next load, with no reinstall. The browser extension's icons follow in its version 0.6.1. In the sidebar, rows rest in a quieter tone and brighten under the pointer, the current place is set in a stronger tone with its icon in your accent, and a group's **All**, **Find** and **+** appear while the pointer or keyboard focus is on its heading (always on a touch screen). On a computer, **More** is a heading that opens its menu, with no icon; the review-due dot is now on **Review** (inside More, or among the primary destinations in Open Loops first) and the unread count on **Updates**, never on More. While you track time, the elapsed time, the task and **Stop** sit in a pill in the sidebar's bottom row instead of a line of their own. On a computer the sidebar column has a faint tint of your accent colour (none in high contrast). - **Settings, tidied.** Cards carry an icon before their title (also in Settings search). Every **Learn more** points like a link and opens Help. Card buttons lose their trailing "…". **How you work** has a **Start guide** row that says whether you finished it and opens the guide over the Inbox. **Recent activity** groups by day, folds repeated sign-ins from one browser into "Signed in ×4", shows ten lines and **Show all**, and names failed or unusual events in the warning tone. The **audit log** is a compact list with filter chips, Export in the header and its retention and address settings in a separate **Audit settings** card. **Date and time** shows one Sample box. The issue connection editor is three steps with **More options**. **API tokens** are rows with scope badges and a create dialog that describes each scope. The **Assistant** card has a status line and "For everyone on this server" and "For you" groups. **AI assistants** is three numbered steps with ready-made configuration for Claude Desktop, Claude Code and other clients. - **One quiet task row.** When a list has timed tasks, their times stand in a column before the titles (a deadline's countdown where there is no time), so a day reads like a schedule; the column's width follows your 12- or 24-hour format. The line under a title is one run of muted text (date, repeat, deadline, start, estimate or `35m / 1h`, blocked, streak, field values) with dots between items, labels are small chips with their colour (two, then "+n") and the only pills of a row, and the project moves to the end of the row next to the counts. With a mouse on a wide screen, hovering a row or tabbing into it shows **Schedule**, **Priority** and **More**; touch screens keep the long-press menu. Board cards, Plan, the review screens and Close the day use the same language. ([Work from the list](docs/work-from-the-list.md#what-a-row-shows)) - **The Rows choice is gone.** Settings → Appearance no longer offers Calm or Detailed rows: there is one row. The stored `row_detail` preference is kept in sync and in exports but no longer read; the task panel and the wide layout's field columns show what Detailed rows used to. - **Filled calendar blocks, neutral events, board columns, settings tiers, statistics tiles.** Calendar blocks, Month and Plan rows and timeline bars are filled in their project's colour, dimmed before today, with a red edge when a timeline task is past its deadline; events from other calendars are neutral with a calendar dot, and a day shows three all-day events then **+N**. Board columns sit on their own surface, cards lift on hover and a dashed space shows where a dragged card lands. Each Settings page keeps one card for its main setting and draws the rest as flat sections, and the statistics tiles show their figure larger above flat panels. - **Calmer colours and sharper headings.** The neutrals are a less blue slate, and the dark theme's canvas is lifted off near-black so cards, panels and menus separate by lightness. Headings are weight 600 with slightly tighter tracking, a new 28 px display step sets the clocks, and Inter's optical sizing is on, so large text uses its Display cut; the font file grows from 54 to 84 KB (one file, cached). Times, counts and durations use tabular figures so they line up. Every colour token is now written in OKLCH and accent themes declare three values from which the rest derive; quieter text uses a lower tone instead of transparency, which the design lint now enforces. ([Design system](docs/design-system.md)) - **Calmer rows.** Hovering a row gives it a faint neutral tint and nothing else; the selected row is marked by a thinner bar and a lighter tint, and hovering it adds nothing. - **Sub-projects read as children.** The sidebar indents each level by 16 px, joins a project's open sub-projects to it with a thin guide line, leaves a little space between rows and after an expanded project's subtree, and shows a sub-project's full path as its tooltip. - **The Inbox holds only what still needs a decision.** A task is in the Inbox while it has no project **and** no plan: a due date, a start date or Someday now takes it out, even without a project, and clearing the date brings it back. A deadline alone doesn't. The same rule applies to the Inbox list and count, Process the Inbox, the Daily check and weekly review, the Loop pulse, `ot inbox` and the MCP `inbox` view, `today_summary` and `opentodo://views/inbox`. Nothing is written to your tasks: the rule reads their fields, so it works offline and every device agrees. The Inbox header says how many planned tasks have no project and opens them in Filter (`no project`); a deferred task without a project or due date is listed in Upcoming on its start day; quick add in the Inbox says where a dated task went ("Added to Friday", with **Open**); and project pickers call the choice **No project**. ([Today and Upcoming](docs/today-and-upcoming.md#where-planned-tasks-go)) - **Schedule from Process the Inbox like from the panel.** s opens the Do on editor's choices, worded field and calendar in your date format, and for a task with a deadline, **On the deadline**, **1 day**, **3 days**, **1 week**, **2 weeks** or **1 month before** on 1–6, leaving out choices already past. Decided tasks stay counted, so "4 of 17" doesn't jump. ([Process the Inbox](docs/process-the-inbox.md#schedule)) - **Inbox zero has a picture.** The empty Inbox and the end of Process the Inbox show a calm line drawing above "Inbox zero: every task has a place.", the first of a set of illustrations drawn only with the theme's colours, in light and dark. - **One page frame.** Every page uses the same frame, at most 80 rem wide: the title and the header's actions stay in the same place when you switch a project between list, board and timeline or go to Labels, Trash or Filter, and lists start at the frame's left edge instead of being centred. Wide boards and timelines scroll sideways inside the frame. ([Interface guide](docs/interface-guide.md#the-page-template)) - **Statistics is a dashboard.** Pick this week, this month, this quarter, this year or last week; every figure is compared with the same days of the previous period ("+12 % vs last month"), in words and an arrow. Four tiles (done, on time, streak, tracked against estimates) lead to panels that each answer one question: am I keeping up (completed and created as two lines), where does my time go (tracked time, or completions, per project), a year of done tasks (53 weeks of shaded days), when do I get things done (weekday by hour), how good are my estimates, and what keeps slipping (open tasks whose due date was pushed back three times in 90 days, from this device's history). Every chart has **Show as table** and can be explored with the arrow keys. The 4- and 12-week periods are gone; a device that last showed one opens on this week. Still computed on your device, with nothing sent anywhere; the charts are drawn by the app, with no chart library. ([Statistics](docs/statistics.md)) ### Fixed - **Opening a task no longer moves the list.** Pages now start at the left edge of the content column, so the task panel opens into the space beside the list and the list stays where it is; before, a page re-centred and slid up to 208 px left when the panel opened. The panel is also 30 rem wide from 1440 px again, as intended (it had stayed 26 rem). ([Interface guide](docs/interface-guide.md)) - **Completing a repeating task in Thunderbird completes it in OpenTodo.** Thunderbird's task list completes one occurrence of a repeating task as a separate entry next to the series, which was ignored; completing the current occurrence now records it in Completed and moves the task to its next date, as completing it in OpenTodo does. Thunderbird and todoman are now verified against a real server ([Calendar apps](docs/caldav.md#limits)). - **Focus rings show where they were missing, and cut text can be read whole.** Keyboard focus now shows the ring on the calendar grid, the Month and Plan day cells, the statistics charts, the timeline, a row's label chips, the workspace and archived-project rows, the task panel's title and Add subtask field, and the sidebar's resize edge. A project name, page title, Trash title, member line or day header that is cut off shows its whole text in a tooltip on hover or keyboard focus. With a mouse, the completion circle, the chevrons, the selection boxes and the sidebar's group actions have 24 px targets, and a row's Schedule, Priority and More no longer cover its title. Lists, project pages and Statistics no longer jump as they load; on a phone the project's line under the title is plain text, and Share in the project menu opens members and public links. The issue connection editor says in the dialog why Test is off until the connection is saved. - **More screens no longer jump while they load.** The Plan, Projects and Updates show their placeholder rows until your tasks are read, instead of an empty page that fills in; the sidebar no longer opens its project tree and then folds it; and a settings page shows its cards together once each has its first answer from the server (at most 0.6 seconds). Nothing moves under reduced motion, now checked on every menu, dialog, sheet and drag. - **Apple Reminders and Calendar can add the CalDAV account.** macOS and iOS ended account setup with "Unable to verify account name or password" because the `/.well-known/caldav` redirect named a relative address and the host root answered their probes with a bare 405. The redirect now carries the server's full address (`OPENTODO_BASE_URL`, or the address the client used), a `PROPFIND` on the root or on `/principals/…` is redirected to `/dav/`, and an `OPTIONS` on the root reports the DAV capabilities. ([Calendar apps](docs/caldav.md#lists-sharing-and-deletion), #249) - **Tasks scheduled in Mitra stay scheduled, and moving them adds no deadline.** A task with a due time is now served with `DTSTART` equal to `DUE`, so clients that schedule by start show it as scheduled; and when such a client moves the task and sends the old `DUE` back unchanged, the task moves instead of gaining a deadline. Equal start and due echoed back are no longer kept as client data. ([Scheduled times and estimates](docs/caldav.md#scheduled-times-and-estimates), #248) - **Files attached from a phone whose clock runs ahead now finish uploading.** The server's "uploaded" mark lost to the phone's own timestamp and was dropped while the upload still answered success, so every device showed "still uploading from another device" for good. The mark now always wins, and an upload that could not be recorded answers an error so the phone retries. (#246) - **Empty dashboard panels and an empty Plan panel say what would be there** ("No tasks match this panel.", "Every task of these days is placed.") instead of "Nothing here.", and the Ask button and the Calendars toggles show their name in the app's tooltip instead of the browser's. - **`ot vault watch` no longer misses a save made during a sync.** A file saved while the watch was syncing counted as already synced, and with the live channel up it waited for the next change in the app; it is now synced on the next check. ([Markdown vault](docs/vault.md)) - **Scheduling clients no longer erase dates over CalDAV.** A calendar app that writes a start and a duration without a due date (Mitra, issue #196) cleared the task's due date and deadline; it now sets the due date from the start and keeps the deadline and the repeat rule. A VTODO without a due date never clears a deadline, and equal start and due keep it too. ([Scheduled times and estimates](docs/caldav.md#scheduled-times-and-estimates)) - **A click selects a task in the Plan.** Clicking a task in the Plan selected it and cleared the selection in the same moment, so the arrow keys moved nothing; the arrows now move the clicked task. - **Task rows line up on phones.** On touch screens the circle sat about half a line above the title; it is centred on the title's first line again, keeping the 44 px tap areas. The layout check now fails when they drift apart. - **"Inbox" means the Inbox.** A dated task without a project no longer carries an "Inbox" chip on its row; the task panel says "No project". - **Subtasks leave the Inbox with their parent.** A subtask without a project or date of its own stayed in the Inbox after its parent was scheduled or moved to a project; it now follows the parent, in the app, the count, `ot inbox` and the MCP tools. - **Scheduling shows your date and time formats.** Process the Inbox and Focus showed a due time as `15:00` even with the 12-hour clock, and a time-zone-fixed due in its own zone; they now follow your formats and your zone. The date field's hint now gives an example in your date order instead of `22/10`. ## [0.9.0] - 2026-10-21 OpenTodo 0.9.0 is about the time you spend in a task, not only the list of them. **Time tracking** and a **Focus** timer log where the hours go, the new **Plan** view lays out the next two weeks against your day length, and **Close the day** wraps up today in one batch. Tasks remember **where they came from** (a page, an email, a GitHub, GitLab, Jira or Linear issue), and assigned **GitHub and GitLab issues** can arrive as tasks by themselves. Everyday editing got calmer: press g and pause to see where you can go, type `#` or `@` for suggestions, click a title to edit it in place, and read the task panel as four short lines with a When editor in plain words. ### Upgrade notes - **One migration, no new environment variables.** Replace the image or the binary and restart; migration 0039 (issue connections) runs on start. There are no down-migrations, so back up first. - **Sync is additive:** new fields (`task.origin`, `view.on_drop`, the `time_entry` entity, `user_prefs.day_start`, `row_detail`, `favourites` and the `focus_*` settings, and `closed_at` and `tomorrow_note` on the review row) are ignored by older apps. Upgrade the server before the devices, as for every release. A task's origin, a time entry's note and the line for tomorrow are encrypted for end-to-end encrypted accounts. - **Issue import is off** until the server owner turns it on; it then makes requests only to the hosts users enter. - **A click on a task's title now edits it in place** on desktop. **Settings → General → Keyboard → Click a task to** brings back opening the panel on that device. ### Security - **Release binaries and the image are built with Go 1.27.3**, which fixes GO-2026-6809 (`crypto/x509` trusted compromised intermediate certificates; OpenTodo reaches it through TLS connections such as outgoing email). ### Added - **A spotlight after `g`.** Pause after pressing g and a small card opens in the middle of the screen: it shows g +, waits for the second key as long as you need, and lists every key that can follow there, grouped into views, calendar, review and planning, and tools. Press one, or click it, to go; Esc, the close button, a click outside or a key that is not listed cancels. Typing g t quickly never shows it. ([Keyboard shortcuts](docs/keyboard.md)) - **`#` and `@` suggest as you type.** In quick add and in a task's title, typing `#` or `@` opens a list of your projects or labels under the field, narrowed on any word of the name; arrows move, Enter or Tab picks, Esc closes. When nothing has that name, the last entry creates it. `#Home/Docs` picks *Docs* under *Home*, a `#name` that matches no project says so in a grey chip instead of staying silently in the title, and a new label reads *New label “urgent”*. ([Quick add](docs/quick-add.md)) - **Edit a task where it is.** Click a task's title in a list (or press F2) to edit it in place, with its date, priority, labels and project in a quiet bar under it; Enter or a click away saves, Esc cancels, Shift+Enter or the arrow opens the panel. Enter on a selected task, touch screens and narrow windows open the panel as before, and **Settings → General → Keyboard → Click a task to** brings back the old click on this device. ([Tasks and projects](docs/tasks-and-projects.md)) - **GitHub and GitLab issues as tasks.** Connect a GitHub or GitLab account (github.com, gitlab.com or your own instance) with a personal access token in **Settings → Integrations → Issues**, and the open issues assigned to you, optionally only in some repositories or with some labels, become tasks in the project you choose. They follow the issue: completed when it closes, reopened when it reopens, renamed when it is renamed, while your own edits stand until the issue changes the same thing again; nothing is ever written back. The server owner turns it on (off by default); tokens stay on the server outside the database and its backups and are never shown again; every request goes only to the host you entered, with no redirects followed, and the private-network rule of webhooks applies. Not available for accounts with end-to-end encryption, and refused on the demo. `ot issues list|sync` and the MCP resource `opentodo://integrations/issues` show the connections without secrets. No new dependency or environment variable; the server binary grows by about 205 KB and the app's main bundle by about 3.6 KB gzipped (2.3 KB per translation, 3 KB of help). See [Issue import](docs/issue-import.md). - **Boards that re-plan what you drop on them.** Give a column of a board of filters a rule in **Arrange** → **Rule** (add or remove labels, set the priority, the date to today, tomorrow, next week or none, Someday, a project and section, complete or reopen), then drag a card onto it, or select a card and press Shift+→ / Shift+←, to apply the rule as one change with **Undo**. A bolt marks a column with a rule; a task that still does not match the column's filter stays shown there, marked **Not matching**, until you leave the board. **New board** now offers an empty board or the **Eisenhower matrix**: four filters by priority and date (Urgent and important, Important, not urgent, Urgent, not important, Neither), each with its rule, on a board pinned to Favourites. Rules sync as one additive `view.on_drop` field, work offline, and are in the views API and the export. ([Saved views](docs/saved-views.md#sort-work-by-dropping-cards)) - **Plan the next two weeks.** A fourth Calendar mode, **Plan** (g Shift+C, or Day | Week | Month | Plan in the calendar's header), shows the next 14 days as columns, seven to a row, each with its tasks (time, estimate, priority) and a capacity bar against your day length that counts task estimates, busy meetings from subscribed calendars (overlaps once) and the coming occurrences of repeating tasks, drawn dimmed and read-only. Drag a task onto another day or move it with the arrows; it keeps its time. Choose 7, 14 or 21 days in the page's menu, remembered per device; on a phone the days are listed vertically. Works offline; nothing new is synced. - **Close the day.** A short end-of-day step at the end of Today, on the Loop home's Review move, in the command palette and with g Shift+D: see how the day went (planned, done as Statistics counts it, today's focus), decide once per unfinished task (Tomorrow by default, a day you pick, Someday, or keep it in Today; t p y h on the selected task) and apply it all as one batch that **Undo** or Ctrl+Z takes back in one step, then leave a line for tomorrow, shown at the top of the next Daily check. Today and the sidebar's Review entry then say **Day closed** with the time. The personal review record gains two additive fields, `closed_at` and `tomorrow_note`; the note is encrypted for an encrypted account. - **Focus.** One task and a large timer at `/focus` (g Shift+F, **Focus on this task** in the task panel's menu, or the command palette), taking the first open task of today's focus and moving on to the next when you complete it. Choose **Pomodoro** (work and breaks with a long break every fourth session, optionally starting each phase on its own), **Flowtime** (counts up until you stop) or **Countdown** (a length you pick each time); Space starts and pauses, Enter completes, b takes a break, n moves on. Each stretch of work is logged as a time entry on the task, breaks never are, and when time cannot be logged the timer works the same and writes nothing. An optional break reminder after 90 minutes of continuous work, a generated tone and a notification when a session ends in a background tab are in **Settings → General → Focus**; the settings sync as additive `user_prefs` fields, the running timer stays on the device. ([Focus](docs/focus.md)) - **Time tracking.** Start and stop a timer on any task (y in a list, the row's actions, the task panel), see it run in the sidebar footer or the phone bar, log past work by hand in the panel's new Time tab, and compare tracked with estimated on rows (`35m / 1h`), in Today's header and in Statistics. One entry runs at a time, also across devices that started offline. Filter with `tracked` and `tracked > 1h`, add spent time in quick add with `30m/2h`, export a CSV from Statistics or with `ot time export`, and use `ot time start|stop|log|list`, the `/api/v1/time-entries` routes and the MCP read tool `list_time_entries`. ([Time tracking](docs/time-tracking.md)) - **Quick add understands ordinal weekdays.** `second Monday`, `the 3rd Friday of November`, `last Friday` and `every 2nd monday of the month` now work, in all six quick-add languages (`am zweiten Montag`, `le 2e lundi de novembre`, `secondo lunedì`, `tercer viernes de noviembre`, `segunda segunda-feira`) and in `ot add --natural`. Before, `Dentist second Monday` became *Dentist second* due next Monday. A bare count before a weekday (`every 2 Mondays`) is not a rule and now stays whole in the title in every language, without a date. ([Quick add syntax](docs/quick-add.md#dates-and-times)) - **My day starts at.** If you work past midnight, set when your day really ends in Settings → General → Date and time (midnight by default, or a whole hour up to 06:00). Until then the app still treats it as the previous day: Today keeps its tasks out of Overdue, a task completed at 01:30 counts for the day before in Completed, Statistics, the Loop pulse and the weekly digest, habits keep their streak, and the Daily check and today's focus last until your day start. Due dates and reminders do not change, and the preference syncs to your devices as an additive `user_prefs.day_start` field. ([Dates and times](docs/dates-and-times.md#when-a-day-ends)) - **Tasks remember where they came from.** A page saved from the share sheet, the bookmarklet or the extension keeps its link as the task's origin: a chip on the row (quiet in Calm rows) and an **Origin** chip in the panel open it in a new tab, and a GitHub, GitLab, Jira or Linear issue link reads as the issue (`o/r#7`, `ENG-123`), recognised on the device without opening it. Email to task records the Message-ID, the Linear importer the issue identifier, and the Todoist, Things and TickTick importers their source id. Filter with `origin`, `no origin` and `origin: github`; find a task in the command palette by its issue id; set it with `ot add --origin-url`, `origin` on the REST API or `origin_url` in the MCP write tools. The origin travels in exports and webhooks, is encrypted whole for end-to-end encrypted accounts, and syncs as one additive `task.origin` field. ([Capturing pages](docs/capture.md#where-a-task-came-from), [Filters](docs/filters.md#terms)) - **A Discord server for questions and conversation.** The welcome note, **Settings → About** (a new Community row, found by searching "discord" or "community") and the Feedback and support guide link to the OpenTodo Discord server. It is a plain link that opens in a new tab; the app loads nothing from Discord. ([Feedback and support](docs/feedback-and-support.md)) ### Changed - **The task panel reads as four lines.** Where (project › section, the parent, the origin), When (the date in the list's words, evening, repeat, the deadline with its countdown, the start, reminders), How (priority, estimate, labels) and Who (assignee, what it waits for, what waits for it). Each part opens its editor next to it with **Clear** and **Done**, and clicking it again closes it; **Add** lists the rest grouped the same way, with **Choose files…** and **Add a link…**. A long title wraps to two lines, long notes show eight lines and **Show all**, and the comment box rests as one line until you click it. ([The task panel](docs/task-panel.md)) - **A When editor you can read.** Choose **Today**, **Tomorrow**, **This weekend**, **Next week** or **No date** in one click, each with the date it means; type `fri` and see "→ Fri 23 Oct" before you press Enter; pick a day in the calendar that is always there; **Add time** suggests 09:00, 13:00 and 18:00, and the time zone is one quiet line once there is a time. Start offers Today, Tomorrow, Next week and Someday, Deadline offers End of week, End of month and In 2 weeks, and Repeat starts from Every day, Every weekday, Every week on Thursday, Every month on day 22 and Every year, with **Custom…** for any rule. ([Dates and times](docs/dates-and-times.md)) - **A task's title understands what you type into it.** Editing a title in the panel or in the list reads only what you typed in this edit: add ` tomorrow p1` and the task is due tomorrow with priority 1, the title stays as it was, and **Undo** takes it all back. An old "Review may report" never gains a May date. ([Quick add](docs/quick-add.md)) - **A compact shortcut sheet.** ? shows one line per shortcut, what it does on the left and its keys on the right, with "then" between the keys of a two-step shortcut and "or" between alternatives, in three columns on a wide screen; keys never wrap or run into the text, in the sheet and in Help. ([Keyboard shortcuts](docs/keyboard.md)) - **A calmer selection.** The selected task is marked by a bar at its left edge and a faint tint instead of a grey box with a ring, and a click on any empty part of the list lets go of it and closes the panel. - **Settings explain themselves in one line.** Every settings card, row and notice now says what it does in one line, and a card whose guide says more ends that line with **Learn more**, which opens the matching guide section beside Settings without leaving the page; the longer explanations moved there, and safety warnings keep a one-line pointer on the card. On the demo, the note at the top of a settings page is one line naming what is disabled, with Learn more to the demo guide. A few long row names became shorter (**Sign out here**, **Week starts on**, **Invite new people**, **Changes to my tasks**, **Small changes without review**), and the Focus card's Help row became its Learn more. ([Interface guide](docs/interface-guide.md#settings)) - **Calm task rows.** Rows now leave out what the view already says (no "Today" in Today, where the time stands alone; no date under an Upcoming day; no "evening" under This Evening; no Someday chip in Someday; no chip for the label whose view is open), colour only what needs attention (the circle for P1 and P2, the date when it is overdue or timed for today), show labels as coloured dots with the first name and "+2", and move subtask progress, comments and attachments to a faint group at the end of the row that brightens on hover. **Settings → Appearance → Rows** has **Detailed**, which restores the previous row with every chip and colour; the choice syncs to your devices like Density. ([Work from the list](docs/work-from-the-list.md#what-a-row-shows), [Settings](docs/settings-you.md#appearance)) - **Less between you and your first task.** The quick add no longer shows a Help link under it; the page header's Help opens the view's guide, which links the quick-add syntax. On the demo the banner is one line at every width, with the sandbox's lifetime from the server, **Learn more**, **Details** with the full text, and **Dismiss** for the browser session; the write-limit notice takes its place while it applies. The first-visit steps leave the Inbox and become a start guide over it, a centred card on a computer and a bottom sheet on a phone, closed for good in any way you close it and shown again from **Settings → Account → How you work**. Empty views say one sentence and offer one action, with the key in the button's tooltip. ([Your first task](docs/your-first-task.md), [Getting started](docs/getting-started.md#try-the-demo)) - **The Loop home reads in one second.** The Open Loops home screen now opens with a capture field, shows the five moves on one rail with a large count each and exactly one move lit as your next move with the page's only button, puts today's focus below with how many of the chosen tasks are done, and says when the loop is closed. A new **Loop pulse** under the focus shows tasks done this week against last week, how many of the items you caught in the last fourteen days have left the Inbox, the age of the oldest Inbox item and completions per day, all computed on the device with the same definitions as Statistics; nothing is stored or sent. ([Open Loops](docs/method/overview.md)) ### Fixed - **The task panel's conversation no longer jumps.** Switching between Comments, Activity, History and Time scrolled the whole panel; the tabs now stay pinned under the header while you read a long thread, switching keeps your place, and comments are spaced as one thread instead of leaving room for hidden buttons. - **Do on toggles.** Clicking the date of a task with its editor open closed and reopened it in the same moment; a second click now closes it. - **The `latest` image reports which build it is.** Images built from the main branch reported their version as `main`; they now report `dev+`, the commit they were built from, in Settings → About, `/healthz` and the log. ([Docker](docs/self-hosting/docker.md), #160) - **No "Open menu" bubble over the menu on phones.** Tapping an icon button (such as the menu button) no longer leaves its tooltip on screen, where it covered the first sidebar entry; tooltips still appear on hover and when you move to a button with the keyboard. (#169) ## [0.8.0] - 2026-10-18 OpenTodo 0.8.0 introduces **Open Loops**, a method of its own for getting everything off your mind, inspired by GTD: catch, clarify, place, do and review. You choose how prominent it is, from not at all to the first screen you see. The weekly review becomes a guided walk through your Inbox, what slipped, what is waiting and every project, joined by a five-minute Daily check and a Monthly look. Nothing changes until you turn it on. Email to task also keeps long messages and turns newsletters into readable, formatted notes. 0.7.2 was prepared but not published on its own: its changes (listed below) arrive with this release. ### Upgrade notes - **No migration and no new environment variables.** Replace the image or the binary and restart. - **Sync is additive:** new preference fields (`user_prefs.method_mode`, `project_prefs.reviewed_at`, and `daily_at`, `monthly_at`, `focus_date` and `focus` on the review row) hold settings and timestamps, never task content; older apps ignore them. Upgrade the server before the devices, as for every release. - **The JSON export now includes per-project preferences** (`project_prefs`, such as when you last reviewed a project), and importing your own export restores them. - **The weekly email digest lists fewer projects as "without a next action"**: see Changed. ### Added - **Open Loops, a calm method for everything on your mind.** OpenTodo now has a method of its own, inspired by GTD: catch, clarify, place, do and review. Choose how prominent it is in **Settings → Account → How you work**: **Lists only** (as before), **Companion** (screens name the move they serve and link to the method) or **Open Loops first** (the new **Loop home** opens first and the sidebar is arranged by the Places). New views: **Next** (g Shift+N), each project's next action grouped by `@context` labels, and **Waiting** (g Shift+W), what you assigned to someone else or is blocked. The method has its own chapter in the docs and Help. ([Open Loops](docs/method/overview.md)) - **The weekly review walks every project, one step at a time.** A stepper shows where you are and what is left; **Next** (]) and **Back** ([) move through seven steps in three phases: the Inbox and a Mind sweep, then what slipped, what is waiting, each project one card at a time (r marks it reviewed, q adds a next action), the week ahead, and Someday. Your mark on a shared project is yours alone. A five-minute **Daily check** (g Shift+R) picks up to three tasks as today's focus, and a **Monthly look** covers your areas, old Someday items and the four-week trend. ([Weekly review](docs/review-and-statistics.md), [Rhythms](docs/method/rhythms.md)) ### Changed - **"Projects without a next action" counts waiting and deferred tasks.** In the review and the weekly email digest, a project whose open tasks are all assigned to someone else, blocked, or start later is no longer listed; one whose only tasks are parked in Someday now is. ([Places](docs/method/places.md#projects-without-a-next-action)) - **Statistics and the weekly digest have their own guide page**, next to the weekly review's. ([Statistics and the weekly digest](docs/statistics.md)) - **Why OpenTodo can be missing from the Android share menu.** The capture guide explains that Android lists only web apps installed as an app from Chrome or Samsung Internet, how to check, and what works instead. ([Capturing pages](docs/capture.md#opentodo-is-missing-from-the-share-menu-on-android), #159) ### Fixed - **Newsletters sent to Email to task are readable.** An HTML message now becomes formatted notes with its headings, lists, bold text and links shown as their text, instead of plain text with every tracking address spelled out; a newsletter's hidden preview text and its blank filler lines are gone. ([Email to task](docs/email.md)) - **Email to task keeps long messages.** A forwarded message's text was cut at 16 KB; the task now keeps about 48 KB, as much as its notes hold (also with end-to-end encryption), and only longer messages end with a note that the rest did not fit. ([Email to task](docs/email.md), #161) ## [0.7.2] - 2026-10-18 OpenTodo 0.7.2 gives the sidebar on a computer more room for your projects: More opens as a menu, Help joins the footer icons, and empty groups stay hidden until you use them. The owner of a new server is greeted with a short thank-you, and a new **Settings → About** section gathers the version, the documentation, the website, the issue tracker and the source code. Unlike 0.7.1, its GitHub release has the binaries attached again. ### Upgrade notes - **No migration and no new environment variables.** Replace the image or the binary and restart. - **Sync is additive:** one new preference field (`user_prefs.welcome`) remembers that the welcome note was closed; older apps ignore it. ### Added - **A thank-you for new servers.** The first time the owner opens a new server, a short note thanks them for installing OpenTodo and links to the issue tracker, the GitHub repository and Buy Me a Coffee. It shows once per account and never to invited members or on the demo. ([Feedback and support](docs/feedback-and-support.md)) - **Settings → About.** A new section for everyone with the server's version, Help, the documentation and the website, the issue tracker, the source code and a Buy Me a Coffee button; the owner can show the welcome note again from there. ### Changed - **A roomier sidebar on a computer.** Search sits next to the OpenTodo name, Help joins the icons in one footer row, and **More** opens as a menu instead of pushing your projects down. Filters and Labels appear once you have one, rows are a little tighter with a mouse, and the sidebar has a thin scrollbar with a soft fade where more is hidden. In a 1280×600 window seven projects now fit instead of two. Touch screens keep their larger targets and the inline More group. ## [0.7.1] - 2026-10-18 OpenTodo 0.7.1 lets a project keep its completed tasks in its own list, in a Completed group you open when you need it, for lists you tick off and reuse such as groceries. It also fixes completed tasks showing an empty circle instead of a ticked one. ### Upgrade notes - **No migration, no new environment variables, no sync or API change.** Replace the image or the binary and restart; `stable` and `:0.7` already point at 0.7.1. - **The GitHub release of 0.7.1 has no attached binaries** (the tag was pushed before this changelog section existed, so the release job stopped after publishing the images). Use the container image, or 0.7.2 for binaries. ### Added - **Keep completed tasks in a project.** Turn on **Show completed tasks** in a project's menu and its completed tasks stay at the end of the list in a **Completed** group, closed until you open it, newest first. Untick one there to put it back on the list, handy for groceries and other lists you reuse. The setting is per project and per device. ([Completed tasks in a project](docs/completed-in-projects.md), #145) ### Fixed - **Completed tasks show a ticked circle again.** In the Completed view, a section's revealed completed tasks and the task panel, a completed task's circle kept its priority colour, which hid the check mark. ## [0.7.0] - 2026-10-18 OpenTodo 0.7.0 is a hardening release, the result of a review of the whole code base. Your account is harder to take over: credential and administration changes need an admin token or your signed-in session, repeated wrong passwords lock password sign-in for a while, a new server cannot be claimed from the internet without a setup code, forwarded addresses count only from trusted proxies, and **Sign out everywhere** ends every session and token at once. Sync keeps your changes in more of the situations where it could lose them (a device with a wrong clock, a proxy answering in the server's place, signing out during a sync, the history of a newly joined project), large servers and long lists answer faster, backups no longer fill the disk and reach S3 above 5 GB, and the app works better with screen readers and voice control. Templates for fourteen more platforms and a `stable` image tag make installing and updating simpler. Read the upgrade notes first: emailed links now need `OPENTODO_BASE_URL`, and scripts that change credentials need an admin token. ### Upgrade notes - **Migrations `0035` to `0038`** (attachments of projects shared earlier move to the project; the `admin` token scope; confirmed pages of a joined project's history; an index for dependencies) are applied automatically at startup. Back up `/data` before upgrading: there are no down-migrations. - **No new environment variable is required, but set `OPENTODO_BASE_URL`** if you have not. Password resets, sign-up confirmations and invitations are emailed only when it is set, and with it set the app must be opened at its host (the sign-in screen names the expected address otherwise). With an https base URL the server also sends HSTS. ([Outgoing email](docs/self-hosting/outgoing-email.md#set-the-base-url-first)) - **Reverse proxies and client addresses.** Proxies on loopback, private or container networks, and `OPENTODO_TRUSTED_PROXIES=cloudflare`, need nothing. If you set `OPENTODO_TRUSTED_PROXIES` to your own addresses, the list now replaces the default: add the new keyword `private` if a reverse proxy on the machine or a container network also connects (for example `private, 203.0.113.10`); a list without any local address shows a configuration warning. A reverse proxy on a public address must be listed, otherwise every visitor shares its address. Behind `OPENTODO_BASE_URL`, open the app at the base URL's host. - **A new server reached from the internet asks for a setup code** before the owner account is created, unless `OPENTODO_SETUP_TOKEN` is set: it is printed in the log (`docker logs opentodo`). From the same machine or your local network nothing changes, and servers that already have an owner are not affected. ([The setup code](docs/self-hosting/docker.md#the-setup-code)) - **API clients and scripts.** A `full` token is refused on the credential and administration routes with `403 insufficient_scope`: use an `admin` token there (see Breaking). Request bodies are checked strictly, so a field a route does not define is refused with `400 bad_request`. Sign-in answers `429 account_locked` after repeated wrong passwords, and password checks can answer `503 busy`. ([API](docs/api.md#behaviour-notes)) - **Sync** stays compatible: upgrade the server before the devices, as for every release. Changes the server makes itself now come from the device `server:` (one id per server process; webhooks still report the source `server`). Apps from 0.7.0 confirm each page of a joined project's history and correct their clock when the server asks; older apps keep the previous behaviour. One new route: `POST /api/v1/me/sign-out-everywhere`. - **Give the container time to stop.** On shutdown the server waits up to 20 seconds for its background jobs. Docker stops a container after 10 seconds by default, so set `stop_grace_period: 30s` in your compose file (or `docker stop -t 30`) to let a backup or a digest finish cleanly. - **Image tags.** The release publishes `ghcr.io/pixari/opentodo:0.7.0` and `:0.7` for `linux/amd64` and `linux/arm64`, and moves `:stable`, which most templates and `docker-compose.yml` now follow. Installs on `:0.6` stay on 0.6: change the tag to `stable` (or `:0.7`) to update. The templates for stores that need a version (CapRover, CasaOS, Easypanel, Helm, Runtipi, TrueNAS, Umbrel) name `0.7.0`. - **Binary size** is about 28 MB (27 MB in 0.6); the installed app's precache stays at about 4.4 MiB. ### Breaking - **Scripts that use a `full` token on credential or administration routes must switch to an `admin` token** (breaking for those scripts only; everything else, including reading these settings and lists, revoking tokens and app passwords, and forgetting remembered browsers, works with `full` tokens as before). Such requests now get `403 insufficient_scope`. Create an admin token in **Settings → Integrations → API tokens → New token**, access **Admin** (after confirming your password if asked), or `POST /api/v1/tokens` with `{"scope":"admin"}` from a signed-in session, and put it in the script instead. `ot token create` needs an admin token in the profile too; `ot login` still works. Existing tokens keep their scope. ([API](docs/api.md#token-scopes), [CLI](docs/cli.md#tokens-and-export)) ### Security - **A server can no longer turn end-to-end encryption off on your devices.** If the server says it has no encryption key record (for example after it was restored from an older backup), your devices keep encrypting and pause syncing instead of uploading your content in plaintext. A screen lets you restore the key record from the device, turn encryption off on that device, or sign out. When you turn encryption off on one device, each of your other devices now asks once before following. ([End-to-end encryption](docs/e2ee.md)) - **Encryption key records with unusual settings are refused.** A device only unlocks with a key record whose key derivation strength is within the expected range; otherwise it stays locked and says why. - **Only the share sheet can hand files to the installed app.** Other websites can no longer store shares in the app through its share target, and a share larger than 100 MB in total is not stored. ([Capture](docs/capture.md)) - **The encryption threat model now notes that the server can show an older version of an encrypted field.** Content stays unreadable and unforgeable to the server, but the app cannot tell when it is served a previous encrypted value of the same field. ([End-to-end encryption](docs/e2ee.md)) - **Pasting into notes is safer.** The rich-text editor's ProseMirror view is updated to 1.42.6, which fixes a published issue with pasted content (GHSA-c8x8-7fp4-3x9w). - **Links are encrypted with the rest of your data.** Turning end-to-end encryption on now also encrypts the address and name of links you attached before, and turning it off decrypts them again. Each link is replaced by a copy in the new form; the old one moves to the Trash. ([End-to-end encryption](docs/e2ee.md)) - **A damaged or tampered backup archive cannot fill the disk during a restore.** Every file in the archive must have exactly the size its manifest lists, nothing more is written, and the server checks that the listed files fit before it extracts anything. ([Backups](docs/backups.md)) - **Images and release files can be verified.** The container image is signed with cosign (keyless, tied to the release workflow) and carries an SBOM and build provenance; every file on a GitHub release has a build provenance attestation for `gh attestation verify`. ([Running with Docker](docs/self-hosting/docker.md#verifying-the-image-and-downloads)) - **A release is published only when every check passed.** The image, its `stable` tag and the GitHub release now wait for the layout, interface, docs website and template checks too; the image is built, scanned for known vulnerabilities (a critical one with a fix available stops it) and smoke-tested before it is pushed. Build tools and base images are pinned to exact versions, and Dependabot proposes updates weekly. - **Release binaries and the image are built with Go 1.27.2**, which fixes standard-library vulnerabilities (net/http, crypto/tls, crypto/x509 and others) in the 1.26.0 toolchain used up to 0.6. The `go.mod` toolchain, the Docker build stage and CI use the same version, the image's web app is built on Node 26, and CI fails when the code can reach a known vulnerability (`govulncheck`). People building from source still need Go 1.26 or newer; the `go` command downloads 1.27.2 for them. - **Security reports go through GitHub's private vulnerability reporting.** See `SECURITY.md`. - **Forwarded client addresses count only from trusted proxies.** `X-Forwarded-For` (and Cloudflare's `CF-Connecting-IP`) are believed only when the connection comes from a trusted proxy: by default loopback, private and link-local addresses, which covers a reverse proxy on the same machine or a container network. A client that reaches the server directly can no longer choose the address that rate limits, the audit log and the demo limit see. IPv6 clients are limited per `/64`. ([Behind a reverse proxy](docs/self-hosting/docker.md#behind-a-reverse-proxy)) - **Repeated wrong passwords lock the account for a while.** Ten failed passwords, two-factor codes or re-authentications for one account within 15 minutes, from any addresses, lock its password sign-in for the rest of those 15 minutes (the app says so; the API answers 429 `account_locked` with `Retry-After`). A successful sign-in clears the count. Passkeys, single sign-on and existing sessions keep working. - **A new server cannot be claimed from the internet without a code.** Without `OPENTODO_SETUP_TOKEN`, creating the owner account from a public address now needs a one-time setup code that the server prints in its log when it starts without an owner (`docker logs opentodo`). From the same machine or your local network nothing changes. The code is forgotten once the owner exists. ([The setup code](docs/self-hosting/docker.md#the-setup-code)) - **Stricter origin check with a base URL.** With `OPENTODO_BASE_URL` set, browser requests must be addressed to its host, so a page under another name that resolves to your server is treated as cross-site. - **Secure cookies over https, and HSTS.** Cookies are marked `Secure` whenever the request arrived over https, also without a base URL. With an https `OPENTODO_BASE_URL`, responses send `Strict-Transport-Security` for one year (no `includeSubDomains`, no preload). - **Password checks can no longer exhaust a small server.** Registration, project invite links and password resets check the sign-up policy, the invitation or link and the setup token before the password is hashed; at most twice the CPU count of password checks run at once, and a request that waits more than a second gets 503 `busy` with `Retry-After`. Sign-in refuses passwords over 1,024 bytes without checking them. ([API](docs/api.md#behaviour-notes)) - **Sign-in timing no longer tells accounts without a password apart** (single sign-on and demo accounts): they cost the same check as an unknown address. - **Live-update streams are capped:** 10 per account (a new one closes the oldest), 20 per client address and 2,000 per server; beyond the last two the stream is refused with 503 `busy` and the app keeps syncing by polling. ([Sync](docs/sync.md#live-updates)) - **One account's push endpoints cannot hold back everyone's reminders.** Reminders are sent for 8 accounts at a time with a 20 s budget per account and pass, an account keeps at most 20 push subscriptions (a new one replaces the least recently registered), and a subscription that failed 20 times in a row and delivered nothing for a day is no longer sent to until the device re-enables notifications. ([API](docs/api.md#reminders-and-push)) - **Calendar data, MCP batches and calendar subscriptions are bounded.** Reading iCalendar data (CalDAV writes and subscribed calendars) takes time in proportion to its size, and one content line may be at most 64 KB. The MCP endpoint refuses JSON-RPC batches under protocol revisions that no longer have them (2025-06-18 and later), accepts at most 20 messages per batch otherwise, and stops running a batch once its answer reaches 4 MB. Expanding a subscribed calendar's repeating events starts at the shown period, shares one work budget and a 5-second deadline per calendar, and keeps what it found with a warning when it runs out. ([CalDAV](docs/caldav.md), [MCP](docs/mcp.md#limits-and-security), [Other calendars](docs/other-calendars.md#limits)) - **Emailed links always use the server's public address.** Password resets, sign-up confirmations and invitations sent by email are built only from `OPENTODO_BASE_URL`, never from the request, so nobody can make the server mail a link to another site. Without `OPENTODO_BASE_URL` these emails are not sent: the screen says the owner must set it, links to copy keep working, and the owner sees a configuration warning while outgoing email is set up. ([Outgoing email](docs/self-hosting/outgoing-email.md#set-the-base-url-first)) - **Forwarded host headers count only from trusted proxies.** `X-Forwarded-Host` and `X-Forwarded-Proto` are used only when the request comes from an address in `OPENTODO_TRUSTED_PROXIES`. Behind a TLS proxy, set `OPENTODO_BASE_URL` (or list the proxy) so the links the app shows start with `https://`. ([Docker](docs/self-hosting/docker.md#behind-a-reverse-proxy)) - **A shared project's id cannot be taken over.** Sharing a project is refused when another account has a project with the same id, so nobody can lock the owner out of their own project. ([Sync](docs/sync.md)) - **Entity ids are limited to safe characters.** Letters, digits and `. _ : @ + = ~ -`, which covers every id the apps create, so a malformed id can no longer break the lists of a shared project's members. ([Sync](docs/sync.md#operation)) - **Credentials and administration need an admin token or your signed-in session.** A `full` API token keeps everything it could do, except: turning two-factor authentication on or off, recovery codes, passkeys, creating API tokens and app passwords, unlinking single sign-on, changing the password, invitations, user administration (roles, deactivation, deletion, reset links, ownership transfer, two-factor resets), the two-factor policy, single sign-on settings, the encryption key record, the outgoing-email, backup and assistant settings that hold secrets, and a hosted server's whole-server archive download. Those now take a session (re-confirmed in the last ten minutes where they already asked for that) or a token with the new **`admin`** scope, which can only be created from such a session. A leaked `full` token can no longer turn off your second factor, mint more credentials or issue reset links. ([API tokens](docs/api.md#token-scopes)) - **Single sign-on never links the owner, admins or accounts with two-factor authentication or a passkey by email alone.** Those accounts sign in as usual and link the provider in Settings → Account → Linked accounts; until then the provider sign-in is refused with "This account is not linked yet". Members without a second factor are still linked by their verified email. ([Single sign-on](docs/self-hosting/sso.md)) - **A password reset forgets remembered browsers**, so every browser asks for the two-factor code again, whether the link came from **Forgot password?** or from an administrator. - **Sign out everywhere.** Settings → Account → Session → **Sign out everywhere** (`POST /api/v1/me/sign-out-everywhere`) ends every session of your account, this one included, forgets remembered browsers and revokes your API tokens and CalDAV app passwords at once. It is recorded in the audit log as `sessions.revoked`. ([Account security](docs/account-security.md#sign-out)) - **Every route declares who may call it.** Each API route is registered with one policy (anonymous with a reason, signed in, owner, owner and admins, accepted token scopes, re-confirmed session, refused on a demo), replacing the separate owner and session checks that disagreed (outgoing email refused tokens while single sign-on accepted them; both now follow the rule above). A test fails for any route without a policy and checks the demo restrictions against it. ### Added - **Templates for six more ways to run OpenTodo.** A Helm chart for Kubernetes (one pod with a persistent volume, an optional Ingress, a generated setup token, a locked-down pod), published with each release to `oci://ghcr.io/pixari/charts/opentodo`; a Podman Quadlet that runs OpenTodo as a systemd service, kept up to date by `podman auto-update`; a CapRover one-click app; a `fly.toml` for Fly.io; a Render Blueprint with a Deploy to Render button; and a Container Manager project for Synology DSM 7.2+. Each is checked by `make check` and has a guide. ([Kubernetes](docs/self-hosting/kubernetes.md), [Podman](docs/self-hosting/podman.md), [CapRover](docs/self-hosting/caprover.md), [Fly.io](docs/self-hosting/fly.md), [Render](docs/self-hosting/render.md), [Synology](docs/self-hosting/synology.md)) - **Templates for eight more platforms.** CasaOS and ZimaOS, Cosmos Cloud, Dokploy, Easypanel, Portainer, Runtipi, TrueNAS Community Edition and Umbrel each get a template in their own format, checked by `make check`, with a guide covering install, settings, data permissions, https, updates and backups. Until a platform's catalog lists OpenTodo, the guide shows how to install it as a custom app; Portainer and Dokploy can already load the templates from this repository by URL. Stores that refuse moving tags name the release version (Umbrel also its digest, refreshed by `scripts/deploy/pin.mjs`); the others use `stable`. ([CasaOS](docs/self-hosting/casaos.md), [Cosmos](docs/self-hosting/cosmos.md), [Dokploy](docs/self-hosting/dokploy.md), [Easypanel](docs/self-hosting/easypanel.md), [Portainer](docs/self-hosting/portainer.md), [Runtipi](docs/self-hosting/runtipi.md), [TrueNAS](docs/self-hosting/truenas.md), [Umbrel](docs/self-hosting/umbrel.md)) ### Changed - **The task list, calendar, task properties and assistant panel are built from smaller parts.** Nothing changes in how they look or work; their grouping rules now have their own tests, and a check keeps new screens from growing into one very long function again. - **For contributors: the deploy template checks share one strict YAML parser** (`scripts/deploy/yaml.mjs`) instead of four that read some files differently, so a quoted `'it''s'` or a folded text means the same in every check. The committed templates are unchanged. - **For contributors: `AGENTS.md` and `docs/architecture.md` name every server package**, `AGENTS.md` describes the MCP server's write tools, and a repository check fails when a package or token scope is left out. The privacy list in the README now also names outgoing email and calendar subscriptions. - **OpenTodo is in Unraid's Community Applications.** Install it from the **Apps** tab: search for OpenTodo and click **Install**. Installing from the template URL by hand still works. ([Unraid](docs/self-hosting/unraid.md)) - **A `stable` image tag, used by every template.** Each release now also publishes `ghcr.io/pixari/opentodo:stable` (release candidates never move it), and the Unraid and Coolify templates, `docker-compose.yml` and the README examples use it: every release, minor versions included, arrives through the platform's update button, with migrations applied on start. Installs on `:0.6` keep their tag and its fixes; switch to `stable` once to follow every release. A **Promote stable** workflow points `stable` at an earlier release to roll back. ([Docker](docs/self-hosting/docker.md), [Unraid](docs/self-hosting/unraid.md#updates-and-backups), [Coolify](docs/self-hosting/coolify.md)) - **Choose the address `docker-compose.yml` publishes on** with the optional `OPENTODO_BIND` in `.env`. The default stays every interface, so existing installs reached from your network keep working; behind a reverse proxy on the same machine, `OPENTODO_BIND=127.0.0.1` is the safer choice. ([Docker](docs/self-hosting/docker.md)) - **Contributors: the server code is easier to find your way in.** The HTTP server, the sync write path, quick add, the importer, CalDAV and the AI agent tools are split into smaller files and functions, with no change in behaviour. A test now fails when a function under `internal/` grows past 150 lines or a file past 1,100 lines. - **Separate rate limits per purpose.** Sign-in, registration, password resets, invitation links, public links, MCP token failures and passkey sign-in each have their own window of 10 a minute per address, and successful sign-ins no longer count, so guessing one kind of link no longer blocks signing in. - **Request bodies are checked strictly.** A field a route does not define is refused with 400 `bad_request` naming it (the sync exchange, push subscriptions and passkey credentials still accept extra fields), data after the JSON value is refused, a body over the size limit gets 413 `too_large`, and error messages no longer show the server's internal decoder text. The app and the `ot` CLI send only defined fields. ([API](docs/api.md#behaviour-notes)) - The last-use time of API tokens and app passwords is updated at most once a minute, which saves a database write on every request. - The outgoing-email settings (`GET`/`PUT /api/v1/settings/smtp`) now accept an `admin` token as well as the owner's session, like the other settings that hold secrets; other tokens are still refused. ### Fixed - **The phone sidebar scrolls with More open.** On a short phone screen, opening More left the sidebar almost no room to scroll, so a swipe moved the page behind it instead of reaching your projects. More now scrolls with Filters, Projects and Labels, and the page behind the open sidebar stays still. - **The quick add sheet treats archived projects like the views' quick add.** Typing `#name` of an archived project no longer behaves differently in the sheet and in a list; every screen now reads projects, labels and shared-project roles from one shared live copy, so they agree with each other and update together after a sync. - **Screen readers hear what the chips say.** Counts, "Has notes", deadlines, group counts and the warning on a title that could not be decrypted were silent or read as bare numbers; they are now read as full phrases ("3 comments", "4 open tasks"). - **Screen readers follow the list.** Moving with j / k moves focus to the selected task, subtasks are read with their level and position, and each complete button names its task ("Complete Buy milk"). ([Keyboard shortcuts](docs/keyboard.md)) - **Page changes are announced.** The browser tab names the page ("Today – OpenTodo"), going to another page moves focus to its heading, and a "Skip to content" link jumps past the sidebar. ([Keyboard shortcuts](docs/keyboard.md)) - **The log says which level it uses.** The startup line now ends with `log_level=`, and with `OPENTODO_LOG_LEVEL=debug` a first debug line says what debug adds (one line per API request). Before, a log read right after a restart looked the same at `debug` as at `info`, so the setting seemed to be ignored. (#128) - **Project pickers no longer list projects twice.** With only a few projects, a picker (such as "Tasks without a project go to" when importing) showed the ones picked before under "Recent" and then again in the full list. Short lists now show each project once; longer lists keep "Recent" on top and head the full list "All". (#136) - **Buttons and menu items are named by the text they show** (WCAG 2.5.3 Label in Name), so voice control finds them by what is on screen: menu items no longer repeat their shortcut or note in the name, group headers are named by their title and count (collapsed or expanded is announced as a state), calendar items, month days and the project's member list read their full description without hiding the visible text, and "Make owner" and "Restore this value" start with their visible words. - **Unraid: Tailscale explained.** The container's **Use Tailscale** switch cannot work with the OpenTodo image (it has no shell for Unraid's Tailscale script, so the container stopped with `exec /opt/unraid/tailscale: no such file or directory`). The Unraid template and guide now say to leave it off and give the steps for Tailscale Serve on the Unraid server, which gives OpenTodo an https address on your tailnet. ([Unraid](docs/self-hosting/unraid.md#tailscale), #123) - **A server opened at an address other than its base URL says so.** After a move to a new domain with `OPENTODO_BASE_URL` still naming the old one, signing in and "Try the demo" were refused with "cross-site request rejected" or "This page expired. Reload it", which a reload does not fix. The sign-in screen now names the address the server is set up for, and the log has one warning per refused host naming it and the base URL. ([Behind a reverse proxy](docs/self-hosting/docker.md#behind-a-reverse-proxy), #125) - **A proxy page no longer loses unsynced changes.** When something between the app and the server (a proxy, a captive portal) answered a sync with its own page, changes made offline were dropped. They now stay on the device and upload on the next sync, and the status says the answer was not understood. - **Changes are kept when a sync turns out to need unlocking.** If encryption was turned on on another device during a sync, changes a server limit held back were lost and refused changes kept their local values. They are now held for later or take the server's values. - **Custom fields survive a server restore.** After the server is restored from a backup, devices now send their custom field definitions back with everything else. - **Refused custom field and link changes are handled.** A custom field change the server refused now shows the server's value, and a refused attachment no longer stops syncing. - **Stopping the server no longer interrupts its background work halfway.** On shutdown the server now waits (up to 20 seconds) for its background jobs before closing the database: a backup in progress is cancelled and recorded as failed instead of failing with "database is closed", and a weekly digest or reminder that went out just before shutdown is recorded, so it is not sent a second time after the restart. - **Backups no longer fill the disk.** A backup checks the free space first and fails at once with "not enough free disk space for a backup" and the space it needs. Snapshots and partial archives left by a crash are removed when the server starts, and backups to a folder on the server are written straight into that folder instead of being copied there from the data directory. ([Backups](docs/backups.md)) - **Email to task leaves other mail clients' deleted messages alone.** On a mail server without the UIDPLUS extension, OpenTodo no longer expunges the mailbox (which removed every message marked for deletion, not just its own); processed messages stay marked read and deleted until the mail server or your client removes them. ([Email to task](docs/email.md)) - **Reminders on completed tasks no longer slow the server down over time.** The server records them as dismissed once instead of checking them again every 30 seconds, and reads due reminders in pages. ([Reminders](docs/reminders.md)) - **Large backups reach S3 and slow destinations.** Archives over 100 MiB are uploaded to S3 in parts, so archives over S3's 5 GB single-upload limit work, and an upload is no longer cut off after 30 minutes: it fails only when the destination stops answering for 5 minutes. Give the S3 key `s3:AbortMultipartUpload` too. ([Backups](docs/backups.md)) - **Old read notifications are removed while the server runs.** Read notifications older than 90 days are now removed daily, not only when the server starts. - **Signing out during a sync no longer leaves the previous account's data behind.** A sync still waiting for the server is stopped before it writes anything more, and the next account starts with a fresh device. Other open tabs of the app reload to the sign-in screen. ([Offline and sync](docs/offline-and-sync.md)) - **A server that stops answering no longer stalls syncing.** Every request now gives up after 30 seconds (10 minutes for file uploads and downloads); the status shows offline and the app tries again shortly, waiting a little longer each time. ([Offline and sync](docs/offline-and-sync.md)) - **Open tabs recover after a server update.** A screen whose code changed in the update reloads on its own, a "Reload to update" prompt appears when a new version is ready or another tab updated the stored data, and an error in one screen or panel no longer blanks the whole app. The server now answers missing app files with "not found" instead of the app page, and only a real network problem shows as offline. ([Troubleshooting](docs/troubleshooting-app.md)) - **Changes that can't be made say why.** Moving a task under one of its own subtasks, a move a section does not allow, or a change that fails to save now shows the reason at the bottom of the screen instead of doing nothing. - **Only one tab syncs.** With the app open in several tabs, one of them syncs for all; the others show its status and take over when it closes. Moving between lists no longer starts a new sync each time. ([Sync protocol](docs/sync.md)) - **Changes held back by a server limit are no longer lost or sent twice** when they are offered again at the moment another change is held. - **A title you are typing stays as you type it** when another device renames the task at the same time; leaving the field saves your title. - **The browser is asked to keep the app's data**, so it is not deleted when space runs low or after weeks without a visit. - **A device with a wrong clock no longer freezes a field.** When a device's clock runs more than five minutes fast, the server now asks it to correct its clock; the app stamps the change again and uploads it, so the edit is kept and later edits from other devices win as they should. The app also stops correcting its clock once the system clock is right again. Older apps keep the previous behaviour. ([sync](docs/sync.md#clocks)) - **Two server writes in the same millisecond no longer cancel each other.** Every change the server makes itself (API, assistant, reminders, imports, email) now uses one clock, and the server's device id carries a suffix per server process (`server:`; webhooks still report the source `server`). - **History of a newly joined project is resent when a page is lost.** The app confirms each page of a shared project's history it applied, and the server sends a page again until it is confirmed. - **Deleting an account unassigns its tasks in other people's projects**, as when a member leaves, so no task keeps a deleted person as its assignee. - **Notifications and webhooks are no longer lost when the app disconnects during a sync.** Once a change is saved, its notifications and webhook deliveries are recorded even if the request was cut off. - **Impossible due dates are refused.** A due date that does not exist (such as 31 February) or a time outside 00:00–23:59 is no longer accepted, so it cannot break recurring tasks and reminders. - **A database migration that would have been skipped is reported.** The server records which migration files it applied and logs an error at startup for a file it never ran; the repository checks refuse a new migration not numbered above those on the main branch. - **Lists and syncs no longer wait for slow writes.** The server reads through a small pool of read-only database connections next to its single writer, so task lists, syncs and exports answer while a large import, purge or backup step is being saved. A second server process on the same data directory (during a rolling update) now waits its turn to write instead of failing with "database is locked". - **Joining a shared project no longer slows down your lists and first sync.** On a server with many users, listing tasks and the first sync of a member of a shared project read every user's data; they now read only the projects you can see (on a 300,000-change server, a task list went from about 460 ms to 54 ms, or under 4 ms for a member with little data of their own). The server also refreshes its database statistics at startup and daily. - **Saving many dependencies at once no longer stalls the server.** A sync that adds hundreds of "blocked by" links to a large project used to hold the database for many seconds (about 10 s for 1,000 links on 10,000 tasks), delaying everyone else; it now takes a fraction of a second. Completing a task also checks for unblocked tasks faster. A new database index is created on start. - **The activity feed loads faster on projects with a long history.** Each page now reads only the recently changed tasks it needs, and never the notes documents, instead of the whole history of every shared project. The events shown are the same. - **Large public project pages, syncs for members of many projects and CalDAV clients cost the server less.** A public link view no longer runs one query per task, listing memberships no longer runs one query per project, and CalDAV multiget and calendar queries no longer scan the whole account for each task asked for. - **A new hosted instance always gets its setup link.** In multi-tenant hosting, creating an instance whose record could not be read back right away replied without its setup token; the reply now keeps the record just created. - **More reliable tests and checks (for contributors).** Every API test now checks the status of each response, `make check` and CI run staticcheck, the server and the web app share one list of synced and encrypted fields that both test suites check, and a randomized test with replayable seeds checks that devices with wrong clocks converge after a project is shared. The demo's "busy" API message now starts in lower case like the others; the app shows its own text. - **Snooze and Dismiss from a notification follow the corrected clock.** On a device whose clock is off, the notification actions now stamp their change with the same server-corrected time as the app, so the change is neither overwritten by an older edit nor refused; a change the server does refuse stays queued for the app's next sync instead of being dropped. - **Enter in a filter moves to its results right after the view opens.** Pressing Enter before the view had read your tasks left focus in the query field; it now moves to the first result once they are loaded. - **Clearing a property you just added removes its chip.** After adding Estimate (or another property) from the Add menu and setting a value, clearing it again now removes the chip instead of leaving an empty one. - **For contributors: the web app is linted for React hook mistakes.** `make check` now runs ESLint with the Rules of Hooks and effect dependencies (`make web-lint`), and the web tests fail on any network request they did not mock. - **Long lists stay fast.** Moving through a list with `j` and `k` now redraws only the two rows involved instead of every row (in a 2,000-task project, 2 rows per key press instead of 2,000). - **Edits and syncs read your tasks once, not once per panel.** The list, the sidebar counts, the project header, labels, calendars and the task panel now share one live copy of your tasks, so each change or sync reads the stored tasks once instead of about six times with a project open. - **Large lists group and encrypt in linear time.** Grouping a list by project and showing a section's completed tasks no longer slow down sharply with thousands of tasks, rows no longer each open their own lookup when encryption is on, and turning encryption on or off no longer re-reads all your data for every batch of 200 items. - **Commands work the same in every language.** In German, French, Spanish, Italian and Portuguese, the Vim `i` command now puts the caret in the task's title, and moving through the list with `j` and `k` releases a project left focused in the sidebar. - **Viewers of a shared project can no longer skip, detach, schedule or estimate its tasks by keyboard.** Shift+K, Shift+O, `s` and `e` now do nothing on a task you can only view, like the other editing keys, instead of making a change the server then undid. - **Today and the sidebar counts move to the new day at midnight** while the app stays open. - **Confirmations are the app's own dialogs.** Making someone the owner, leaving a project, revoking an invite link, undoing an import and requiring two-factor authentication ask in the app instead of the browser's pop-up. Adding a link to a note (Ctrl/Cmd+K) opens a small dialog that accepts only `https://`, `http://` and `mailto:` addresses, instead of silently dropping anything else. - **The Logbook's "done" count and the estimate field's example are translated.** - **The calendar's hour lines follow the theme**, including high contrast and dark. - **More list actions say why they failed.** Bulk keys, park, duplicate, "this occurrence only", adding a subtask and completing from the panel now show a message when the change cannot be saved. - `SECURITY.md` now says, as the server does, that the audit log takes the client address from the last `X-Forwarded-For` hop (the one your proxy appended), with `OPENTODO_TRUSTED_PROXIES` for proxies in front of it. - **Old repeating events in subscribed calendars.** A daily event that started decades ago (before about 1972) showed no occurrences; it now shows every day of the period. - **Attachments move with a project when it is shared.** Files added to a project's tasks before sharing it stayed visible only to the owner. Sharing now moves them with the tasks, and on upgrade the server moves the files of projects shared earlier; members' devices fetch them on their next sync. ## [0.6.1] - 2026-10-17 OpenTodo 0.6.1 makes plain-http installs work: opened by a home server's or NAS's address (`http://192.168.1.10:8080`), the app no longer shows **Offline** with nothing saved. It adds an **Unraid template**, and fixes encryption turning off part-way, settings pages scrolling away, and two small things in the task panel. ### Upgrade notes - **No migration, no new environment variables, no sync or API change.** Replace the image or the binary and restart. - **Image tags.** The release publishes `ghcr.io/pixari/opentodo:0.6.1` and moves `:0.6`, so installs that follow `:0.6` (Coolify, Unraid) get it from their usual update. ### Added - **Unraid template.** A Community Applications template sets up the container on Unraid: the release image, the Web UI on port 8080, the appdata folder on `/data`, and the optional settings with descriptions; nothing is required. Until it is listed in **Apps**, add it from the template URL. The template is published in its own template repository, [pixari/opentodo-unraid](https://github.com/pixari/opentodo-unraid), from a checked copy in `deploy/unraid/upstream/`. The guide covers appdata permissions (the image ignores `PUID` and `PGID`), https behind a reverse proxy, updates and backups. ([Unraid](docs/self-hosting/unraid.md)) ### Fixed - **Repeat says what it accepts, and the time zone button shows once.** The repeat field takes any interval or pattern, such as `every 3 months` or `every 10 days after completion`, not only the suggestions; it now says so under the empty field, suggests `every 3 months`, and keeps its example rule in English in every language (rules are written in English). In the task panel the time zone button appears beside a due date only when the due has a time, and no longer twice while the When editor is open. ([Recurring tasks and reminders](docs/recurring-and-reminders.md)) - **Plain-http installs on a network address work.** Opened as `http://
:` on a home server or NAS (for example Unraid), the app showed **Offline** and nothing you added was saved, because it used a browser function that only exists over HTTPS. Adding and editing, sync and attachments now work over plain http too; installing the app, push notifications, passkeys and end-to-end encryption still need HTTPS. ([Docker](docs/self-hosting/docker.md)) - **Settings no longer scroll past the end of the page.** On General, Integrations, People and Server, the whole app could be scrolled up and away, leaving an empty page below the last card. Labels that are only read out by screen readers sat outside the scrolling area and stretched the page; they now stay inside it. The layout check fails any screen where the page itself scrolls. - **Turning encryption off no longer stops part-way.** With a task or project note on the account, the re-write to plaintext (and, on a fresh page load, the re-write to ciphertext) failed after its first batch with "Transaction has already completed" when the note code still had to be fetched: the fetch let the browser close the database transaction under it. The code is now loaded without letting the transaction go, and an interrupted run still resumes where it stopped. ([End-to-end encryption](docs/e2ee.md)) ## [0.6.0] - 2026-10-16 OpenTodo 0.6.0 speaks your language and looks back as well as ahead. The interface comes in **English, German, French, Spanish, Italian and Portuguese**. **Statistics** and a guided **weekly review** show what you finished, what slipped and what has no next action, computed on your device, with an optional weekly **digest** by email. **Filter by completion date** (`completed = last week`) and a day-by-day **Completed** logbook answer "what did I do this month?". **Habits** keep a streak and a month history, **custom fields** add typed values to a project's tasks, and **display mode** puts a view full screen on a wall tablet or an e-ink panel. Importers from Todoist, TickTick, Linear and Things keep more of what you had, and operators can run many OpenTodo servers in one process with **host mode**. Clicking a task's circle completes it again. ### Upgrade notes - **Migration `0034`** (weekly digest settings) is applied automatically at startup. Back up `/data` before upgrading: there are no down-migrations. - **No new environment variables are required.** Host mode is off unless `OPENTODO_HOST_URL` is set; `OPENTODO_HOST_TOKEN` (required only with it), `OPENTODO_HOST_OPERATOR_ADDR`, `OPENTODO_HOST_IDLE_MINUTES`, `OPENTODO_HOST_MAX_OPEN` and `OPENTODO_HOST_LIMITS` configure it. A self-hosted server behaves exactly as before. - **Sync** is additive: the entities `field` and `review`, the task fields `habit`, `skipped`, `completed_by` and `fields.`, and the `user_prefs` field `language`. Older clients ignore them; upgrade the server before the devices, as for every new field. - **New routes:** `/api/v1/fields`, `GET`/`PUT /api/v1/me/email-digest`, `GET /api/v1/export/archive` (owner), and `completed_after`/`completed_before` on `GET /api/v1/tasks`. All require authentication; the digest settings write is refused on the demo. - **The installed app downloads the five language catalogs once** (about 100 kB gzipped each) so a language switch works offline; the precache grows from about 2.7 to 4.4 MiB. - **Binary size** is about 27 MB (24 MB in 0.5): mostly the language catalogs. - **Image tags.** The release publishes `ghcr.io/pixari/opentodo:0.6.0` and `:0.6` for `linux/amd64` and `linux/arm64` (`:latest` follows `main`). The Coolify template now follows `:0.6`; if you use `:0.5`, change the tag in **Edit Compose file** and click **Deploy**. ### Added - **The app in your language: English, German, French, Spanish, Italian and Portuguese.** **Settings → General → Language** picks the interface language for every device of the account (or **System**, the browser's first shipped language), and the app switches at once, without a reload, also offline once a language has been loaded on the device. Month and weekday names, the clock and the date order on **System** follow it with the browser's region kept (Swiss German stays Swiss German); quick add and voice capture try it first; reminder notifications (**Snooze 10 min**, **Dismiss**, the due date) are written in it by the service worker; server errors are shown in it by error code; the browser extension follows the browser's language. Tasks, project and label names, filter queries and repeat rules are never translated, and the documentation and the Help guide stay English (Help says so). Each language is a separate chunk of about 100 KB gzipped, loaded only when chosen and precached for offline switching; the main chunk grows by about 6.5 KB gzipped. Sync is additive (`user_prefs.language`); `make check` now runs catalog and untranslated-string checks. ([The app in your language](docs/language.md), [Translating OpenTodo](docs/translating.md)) - **Custom fields per project: text, number, date, choice, link and checkbox values on tasks, filterable, exported, in the API and the assistant tools.** **Fields…** in a project's menu (Shift+F) defines up to 30 typed fields, for editors and above; each task of the project then has a **Fields** group in its panel, rows and board cards show up to three values as badges, and on a wide screen the list offers a **Columns** layout, remembered per project on the device. Values work offline and converge field by field: two people editing different fields of one task both keep their change. Filter with `cost > 100`, `client = "Acme"`, `"Order date" <= friday` or `no client`. A task moved to another project keeps its values, hidden until it comes back; deleting a field hides its values without touching them. The REST API has `/api/v1/fields` and a `fields` object on tasks, the export and own-export import carry both, webhooks deliver the values, and the MCP tools list a project's fields and set values. On end-to-end encrypted accounts field names, options and text values are encrypted. Sync is additive (the `field` entity and the task's `fields.` values); upgrade the server before the devices, as for every new field. ([Custom fields](docs/custom-fields.md), [Filter queries](docs/filters.md#custom-fields), [API](docs/api.md#custom-fields)) - **Statistics and weekly review: on-device numbers, a guided review, an optional weekly digest by email.** **Statistics** (g Shift+S, under More) shows for this week, last week, the last 4 or 12 weeks what you completed (each day of a recurring task once, skipped habit days never), created, completed late and let slip, and what is overdue, with bar charts per day, week, weekday and project; everything is computed on the device from your tasks, offline, and nothing is sent or stored. In shared projects a completion counts for whoever completed it (`completed_by`), the assignee for older tasks. **Review** (g r) walks four steps with live data: get the Inbox clear, look back at the week and what is overdue, give every project a next action or archive it, and look ahead seven days; **Finish review** syncs to your devices and the sidebar marks a review as due after seven days. In **Settings → Notifications** each user can opt in to a weekly digest email on their review day (needs outgoing email; not offered with end-to-end encryption, and turning encryption on switches it off; refused on the demo). Sync is additive (a personal `review` entity); migration `0034` adds the digest settings table; `GET`/`PUT /api/v1/me/email-digest`. ([Weekly review and statistics](docs/review-and-statistics.md), [API](docs/api.md#weekly-digest)) - **Host mode: one process serves many OpenTodo servers.** For operators of a hosted service: with `OPENTODO_HOST_URL=https://example.cloud`, each tenant is an ordinary data directory answering at `.example.cloud`. Tenants open on their first request (running their migrations), close when idle unless a device holds a live stream, and at most `OPENTODO_HOST_MAX_OPEN` stay open. One scheduler runs reminders, backups, Trash retention, webhooks, calendar fetches and weekly digests for tenants with work due only, so thousands of idle tenants cost nothing. Per-tenant limits on storage, changes per hour and in total, requests, import size and members refuse single changes with a named reason while the rest keeps syncing; the app keeps a refused change and shows it as waiting. Outgoing email, single sign-on, backup destinations, attachment limits and the AI switches belong to the operator. A token-authenticated operator interface on its own address creates tenants (empty with a setup link, or from any backup archive), suspends, resumes, deletes and exports them. ([Host mode](docs/self-hosting/host-mode.md)) - **Owners of a hosted server can take their data away.** **Settings → Server → Download your data** saves the whole server as one backup archive that `opentodo restore` reads, so moving to a self-hosted server takes one download. ([Host mode](docs/self-hosting/host-mode.md#owners-take-their-data-away), [API](docs/api.md#hosted-servers)) - **Habits: track a repeating task as a habit with a streak and a month history; skip keeps the chain.** Turn on **Track as a habit** under a task's repeat rule (any rule except `after completion`). Its row shows a streak chip, and the task panel shows the current and best streak and a month grid of done, skipped, missed and upcoming days, navigable with the keyboard. Everything is computed on the device from the series' occurrence records, offline too. Skipping a habit records the day as skipped, so the streak holds; a scheduled day that passes without a record breaks it. Skipped days are not completions: they stay out of Completed and `completed` filters and never count towards `for N times`. The new `habit` filter keyword makes a Habits saved view one word. Sync is additive (the task fields `habit` and `skipped`); upgrade the server before the devices, as for every new field. ([Recurring tasks and reminders](docs/recurring-and-reminders.md#track-a-habit), [Habits reference](docs/recurrence.md#habits)) - **Filter by completion date.** `completed = last week`, `completed >= 7 days ago` or `##Work & completed >= this month` list what you finished; a completion date implies completed tasks. Every date term also takes periods: `this week`, `last week`, `next week`, `this month`, `last month`, `next month`, `2 weeks ago`, `3 months ago`, where `=` means within the period and weeks start on your first day of the week. Saved views keep the period, so "last week" moves with the calendar. Closes #86. ([Filter queries](docs/filters.md#periods)) - **Completed is a logbook.** Completions are grouped by the day you finished them (Today, Yesterday, then the date) and **Show more** reaches older days instead of stopping at the latest 100. ([Views](docs/built-in-views.md)) - **Completion ranges for scripts and assistants.** `GET /api/v1/tasks?completed_after=…&completed_before=…`, the same arguments on the MCP `list_tasks` tool, and `ot list --completed-after … --completed-before …` take a date, an RFC 3339 time or Unix milliseconds. ([API](docs/api.md#tasks-completed-in-a-range), [MCP](docs/mcp.md), [Command-line client](docs/cli.md)) - **Who completed a task.** Tasks gain an additive `completed_by` field (the account id, written with every completion and cleared on reopen, null for public link ticks and older completions); the API and exports return it. Older clients keep syncing. ([Sync protocol](docs/sync.md)) - **Display mode.** Today, Upcoming, a project or a saved view full screen in large type with a clock, for a tablet on the fridge or an e-ink panel: open it with **Display mode** in the view's header or menu, leave it with **Exit** or Esc. Today shows overdue and today's tasks, what was done today ticked below, and the next days as columns; it follows other devices within seconds, keeps working offline with a "Last synced" line, and rolls over at midnight by itself. Options live in the address so a bookmark keeps them: `size=m|l|xl`, `contrast=high`, `motion=none`, `days=1..7` and `eink=1` (high contrast, no motion, a clock without seconds). Tasks can be ticked by touch or with Space only where the account may edit them, so a dedicated viewer account makes a safe shared screen. A display can be the device's start view, and a public link accepts `?display=1` for a panel with no account on it. No server change. ([Display mode](docs/display-mode.md)) - **High-contrast and no-motion presentation** for contributors: `data-contrast="high"` on the root overrides the tokens with pure white or black surfaces and 7:1 text, `data-motion="none"` makes every transition instant; both combine with any theme and accent, and the contrast check measures the override. ([Design system](docs/design-system.md#high-contrast-and-no-motion)) ### Fixed - **Clicking a task's circle completes it again.** Since 0.5.0 the circle turned into a selection checkbox while the pointer was over the row, so a click meant to complete a task ticked it for bulk actions instead. The circle now always completes; selection checkboxes appear only once a task is ticked (Ctrl/⌘+click or Shift+click a row, or x) or in touch Select mode. ([Bulk actions](docs/bulk-actions.md)) - **Imports from TickTick and Todoist keep more of what you had.** TickTick tasks keep the time they were created and completed (the backup writes times as `+0000`, which was not read), and a reminder on an all-day TickTick task fires at the time you set (`PT9H` is 09:00 on the day, not 18:00). A Todoist task with several comments lists them under one "Comments" heading, and an absolute Todoist reminder fires at its time in the task's time zone. Each named format now has a second, richer test file covering recurring tasks, reminders, deadlines, checklists, completed and canceled items and boards. ([Import formats](docs/import.md)) ## [0.5.0] - 2026-10-16 OpenTodo 0.5.0 is a new interface on the same data. The task panel is part of the frame and reads like a summary, the sidebar is shorter, Settings has nine sections with a search field, every keyboard shortcut comes from one registry and ? lists the ones for where you are, and on a phone there is a tab bar, a **+** button and full-screen tasks. Around it: **Process the Inbox** one task at a time, a rich-text **note** at the top of every project, **public links** that share a project's list with anyone, optional **outgoing email** for password resets, verified sign-up and invitations, a **Month** calendar and a project **Timeline**, calendar feeds in both directions, attachments, tasks that wait for other tasks, bulk changes, duplicates and templates, and search and commands on Ctrl+K. Every help page is now short and opens with a screenshot. ### Upgrade notes - **Migrations `0031`, `0032` and `0033`** (calendar feeds, public links, pending sign-ups) are applied automatically at startup. Back up `/data` before upgrading: there are no down-migrations. - **No new environment variables are required.** `OPENTODO_CALENDAR_FEEDS`, `OPENTODO_SMTP_*` and `OPENTODO_ATTACHMENTS_*` are optional; everything they configure is off, or has a default, until you set it or the owner turns it on in Settings. - **Sync** is additive: the project fields `icon`, `description` and `notes_doc`, the task field `blocked_by`, the `attachment` entity and the `user_prefs` fields `sidebar_destinations`, `density` and `first_run`. Older clients ignore them; a 0.4 device keeps syncing. - **Two families of anonymous routes are new:** `GET /api/v1/public/{token}` with `POST …/tick` (public links) and the password-reset request and sign-up verification routes (outgoing email). All are rate limited, re-check their secret on every use, and are refused on the demo. Nothing is reachable without a link or without SMTP configured. - **Settings moved.** The fourteen sections of 0.4 are nine; old addresses such as `/settings/security` and old help ids open the right section at the moved rows. The activity feed is **Updates** at `/updates` (old links redirect). Owner settings for attachments, outgoing email and the audit log live under **Server**. - **Some shortcuts changed.** Skip occurrence is Shift+K, Shift+↓ / Shift+↑ extend a selection, . opens the row's menu; the keys printed in the app return with **Show shortcut hints**. Press ? for the current list. - **Binary size** stays at about 24 MB. (The email templates briefly pulled in `text/template`, whose reflection stops the linker from pruning unused code and had grown the binary to 37 MB; they are plain placeholders again.) - **Image tags.** The release publishes `ghcr.io/pixari/opentodo:0.5.0` and `:0.5` for `linux/amd64` and `linux/arm64` (`:latest` follows `main`). The Coolify template now follows `:0.5`; if you use `:0.4`, change the tag in **Edit Compose file** and click **Deploy**. ### Changed - **A calmer frame.** The task panel is now part of the app's frame: it stays put while the list scrolls, keeps its task in the address (reload or share a link and it reopens), and opens the same way from every list, the calendar, filters and dashboards. The sidebar scrolls as one, with Help and your account always at the bottom and Labels as their own group. Task rows line up across every view and show a second line only when there is something to show. The duration filter lives under a **Filter** button in the list's header, with one chip while it's on; the Inbox no longer labels every row "Inbox". Press ? for a searchable cheat sheet that lists the shortcuts for where you are first. ([Views](docs/views.md), [Keyboard shortcuts](docs/keyboard.md)) - **A task panel that reads like a summary.** The checkbox and title stay at the top while you scroll, with **Expand** opening the task as its own page. Notes are one quiet line until you click them. The properties are chips that read their values, with **Do on** and **Priority** always there and everything else under **Add** (each entry with its shortcut); a chip opens its editor in place, and the date chips share one **When** editor for the due date, start, deadline, evening, repeat and reminders. Subtasks are a checklist with a quiet add line, attachments start as one **Attach** action, the comment box stays at the bottom of the panel, and Activity starts with when the task was created and completed. Completing, deleting, moving, rescheduling or parking a task offers **Undo** in a toast, offline too. ([Tasks and projects](docs/tasks-and-projects.md#edit-a-task)) - For contributors: a page template, a tooltip and skeleton, sidebar building blocks, a style lint over the screens and an interface budget every screen change states ([Interface guide](docs/interface-guide.md)). - **A shorter sidebar and clearer names.** The sidebar starts with Inbox, Today, Upcoming and Calendar; Anytime, Someday, Assigned to me, Completed, Updates, Trash and Filter wait under **More** (the unread count shows on its header while it is closed), and any of them can be promoted next to the primary four from **Settings → Preferences**. **Calendar** is one destination that remembers Day or Week, with its planning list in the panel. The sidebar's "Views" group is now **Filters**, the activity feed is **Updates** at `/updates` (old links redirect), and Settings → You → Activity reads **Sessions and audit log**; the search box still answers to the old words. Every page shares one header with its title, actions, a **⋯** menu and a **Help** button that opens the guide for that screen; projects keep Share and the List/Board switch in it, with Activity and History opening in the panel from the menu, and sub-projects listed as a collapsible group above the tasks. Lists hide empty date groups and an empty "No section", keep a section with only completed tasks as "n completed" with **Show**, and remember which groups you collapsed on each device. Help opens on its search with eight common tasks before the guides, and quick add links to its guide for your first three tasks. ([Views](docs/views.md), [Settings](docs/settings.md)) - **Settings in nine sections.** Settings is one layout: the list of sections (your profile, a **Search settings** field, the groups You, App, Data and Server) is the left column on a wide screen with **Account** already open, and the whole screen on a phone where each section opens as a page with a back link. The sections are **Account** (profile, sessions, password, two-factor, passkeys, linked accounts, encryption), **Appearance**, **General**, **Notifications**, **Capture**, **Integrations** (now with the assistant and AI assistants), **Data** (with archived projects), **People** (users, members, access policies and single sign-on) and **Server** (backups, attachments, the audit log). Old addresses such as `/settings/security` open the right section at the moved rows. Search finds a setting by name or by what it does, in Settings and in the command palette ("passkey" opens Account at the passkeys row). Every card has the same shape: a heading, one sentence, rows with the state as a badge beside the label, and the **Help** button sits once in the page header. **Appearance** gains **Density** (Comfortable or Compact, synced, touch targets kept on phones) and the sidebar switches. On the demo, every section is listed and the rows it refuses are shown disabled under one note. ([Settings](docs/settings.md)) - **One shortcut registry.** Every keyboard shortcut is declared once, with the scope it belongs to (everywhere, a view, a task list, the board, the calendar, the task panel, the notes editor or the Vim layer), and one dispatcher resolves a key in the innermost scope on screen, so a key runs one action and never two. The ? cheat sheet, the keyboard guide and the shortcut column of the command palette all read the registry. Resolved clashes: **Skip occurrence** is now Shift+K (in a list and in the panel) while Shift+S stays **Share project**; s schedules in a list and saves in Filter; . opens the focused row's menu or the Actions menu; a adds a subtask in a list and a card in a board column; Shift+↓ / Shift+↑ extend a ticked range (Shift+J / Shift+K no longer do). Shortcut hints live in tooltips, menus and the palette; the keys printed in the app itself (quick add's q, the sidebar's g keys, prompts) return with **Show shortcut hints** in Settings → General → Keyboard, per device. The palette groups its results under Recent, Tasks, Projects, Labels, Filters, Commands and Settings. ([Keyboard shortcuts](docs/keyboard.md)) - **Built for a phone.** Below 768 px a bar at the bottom of the screen switches between Today, Inbox, Upcoming, Search and More (the sidebar as a drawer), and a round **+** button adds a task from any page as a bottom sheet with the page's defaults and the keyboard up; both step aside while a sheet or a task is open. A task opens as a full-screen page with Back, or a swipe down on its header, to close it. A project's board shows one column at a time with a dot pager and the column's name under the title, and "No section" is a column only while it has open tasks. The Week calendar is a day-by-day list on a phone. The sidebar's project chevrons are full-size touch targets. ([Getting started](docs/getting-started.md), [Installing the app](docs/install-app.md)) - **Shorter help pages.** Every guide in the app's Help is now under 600 words and opens with a screenshot: the long guides (tasks and projects, views, sharing, time blocking, the assistant, settings, notifications, import, trash, troubleshooting and more) became short overview pages that link to one page per topic, 31 new pages in all, and every old address and section link still lands. The docs check enforces the length and the screenshot. ([Help inside the app](docs/getting-started.md#help-inside-the-app)) - **Rows that read at a glance, and a first run.** Every task row is built from the same parts: the date reads "Today · evening · repeats" in one tone, a deadline carries its flag, and the estimate, progress, comments, attachments and project are quiet chips. Completing a task plays a short tick before the row leaves (none under reduced motion), the selection box takes the checkbox's place on hover without shifting the title, and a dragged row lifts over a dotted drop line. Lists, panels and settings show skeletons instead of "Loading…" while local data arrives. The first time an account opens the app, the Inbox shows a three-step card (first task, light or dark, install the app) with **Skip all** and **Show me around**, which creates one sample project of six tasks you can delete like any other; the card never returns on that account, on any device. Every empty state names its action with one button, with the key hint only where there is a keyboard. For contributors: the legacy `btn-*`, `.input` and `.kbd` classes are gone and the style lint covers every screen, the core screens have stories and visual baselines, dark surfaces keep a visible hairline, and `make screens` captures before/after images of the seeded fixture for a pull request. ([Getting started](docs/getting-started.md), [Interface guide](docs/interface-guide.md)) ### Added - **Process the Inbox.** **Process** in the Inbox header (or g p) shows one task at a time and a key for each decision: t today, s schedule, m move, a delegate to someone in a shared project, l labels, y Someday, c complete, Delete trash, j skip, k back. Every decision has Undo, tasks captured meanwhile join the end, and it works offline and with Vim keys. ([Views](docs/views.md#process-the-inbox)) - **Outgoing email (optional).** Point the server at an SMTP server in **Settings → Server → Outgoing email** (or with `OPENTODO_SMTP_*`), check it with **Send test email**, and it can mail what you used to copy by hand. People reset a forgotten password themselves from **Forgot password?** on the sign-in screen. With open sign-up, a new address is confirmed by email before the account exists. Invitations and project invite links have **Send by email**, and the Users panel offers **Send reset email** beside **Create reset link**. Messages come in the six app languages and never carry task content. Nothing changes without SMTP. Migration `0033`. ([Outgoing email](docs/self-hosting/outgoing-email.md)) - **Project notes.** Every project can carry a note at the top, with headings, lists, check lists and links: the goal, the links you keep looking up, a packing list. Click it or press Shift+P to edit it in place with the task-notes editor; two people can edit at once and keep both changes. Owners, admins and editors can edit; viewers read it. Long notes fold per device, history can restore an earlier version, and the API and exports carry it as the project's `description`. ([Project notes](docs/project-notes.md)) - **Public links.** Share a project's list with anyone, no account needed: **Share → Share with a public link** creates a link that is view-only or lets people tick items off, optionally named and with an expiry. The page shows open tasks by section with due dates and priorities, never notes, labels or names, and refreshes itself. Ticks reach every device and read **Public link** in history and activity. A **Public** badge marks published projects; revoking a link stops it at once. Not available for end-to-end encrypted projects or on the demo. Migration `0032`. ([Public links](docs/public-links.md)) - **Repeat and remind from quick add.** Type `every Monday`, `every 2 weeks after completion` or `remind me 30 min before` (also `jeden Montag`, `erinnere mich…` and the other four quick-add languages) and quick add sets the repeat and the reminders, each shown as a chip you can dismiss. A repeating task without a date starts on the rule's first day. The `ot` command line gains `--repeat`, `--remind` and `--natural`. ([Quick add](docs/quick-add.md), [Command-line client](docs/cli.md)) - **A month at a glance, and projects on a timeline.** Calendar now has **Month** next to Day and Week (g m): six weeks of due dates with today marked, "+N more" on busy days and the planned total against your day length. Drag a task to another day, or select it and use the arrows, and it keeps its time; q adds a task on the focused day. Projects get a third layout, **Timeline** (v cycles list, board and timeline): each task is a bar from its start to its deadline, with arrows between tasks that wait for each other. Drag a bar to move its dates, or its ends to set the start and deadline, and use the arrow keys for the same. Both work offline. ([Time blocking](docs/time-blocking.md#see-the-month), [Sections and boards](docs/sections-and-boards.md#see-a-project-on-a-timeline)) - **Calendar feeds, both ways.** Subscribe to the iCal address of your Google, Outlook, Apple, Fastmail or Nextcloud calendars under **Settings → Integrations → Calendar feeds**. Their events then appear read-only in Day and Week, next to your tasks: timed events as blocks in the calendar's colour, all-day events above the unscheduled lane, and free events as an outline. They share the overlap marker but never count toward the day's total. The **Calendars** button hides them on a device, and they stay visible offline. The other way, **Create feed URL** gives you a private address for Google Calendar, Outlook or Apple Calendar that shows your dated tasks, with Rotate and Revoke. The owner turns it on (`OPENTODO_CALENDAR_FEEDS=true` presets it); private network addresses follow the webhooks setting, and the demo refuses it. Migration `0031`. ([Calendar feeds](docs/calendar-feeds.md)) - **Project icons and a sidebar that fits.** Give a project an icon from its menu, next to the colours; it shows in the sidebar, the project header and task rows. Drag the sidebar's edge to make it 15 to 22.5 rem wide, or press [ to collapse it to icons and back (on wide screens). ([Tasks and projects](docs/tasks-and-projects.md)) - **Change several tasks at once.** Tick tasks with x or Space, extend with Shift+↓/↑, tick everything with Ctrl/⌘+A, Shift+click a range, or tap **Select** on a phone. A bar shows how many are ticked; **Actions** (or .) completes, sets priority, labels, a date, moves, assigns or trashes them all in one batch that works offline and syncs as ordinary changes. Moves and assignments are shown for review first; everything else applies at once with **Undo** (also Ctrl/⌘+Z). Works in every task list, the Filter view, saved views and from the search box. ([Tasks and projects](docs/tasks-and-projects.md#change-several-tasks-at-once)) - **Duplicate projects and tasks, and start projects from templates.** **Duplicate…** in a project's menu copies its sections, sub-projects and open tasks, optionally moving every date so the work starts on a day you choose; Shift+D (or **Duplicate** in the task menu) copies a task with its subtasks right below it. **Save as template…** downloads a project as a small file, and **From a template…** (under **+** next to Projects) starts a fresh project from such a file, including the downloads on open-todo.app, with dates counted from your start day. Copies are made on your device and work offline; completed tasks, comments, reminders and assignees stay behind. ([Templates](docs/import-export.md#templates)) - **Tasks that wait for other tasks.** Open a task and choose **Blocked by** (or press b on a row) to pick the tasks it waits for. While any of them is open, the task is dimmed with a lock and a count, in lists and on boards, and Today keeps it in a folded **Blocked** group until it's ready. **Blocks** lists what waits for a task. Completing the last blocker unblocks it on every device, and in a shared project the assignee gets an "is ready to start" notification. Filter with `blocked` and `unblocked`. Calendar apps see the links as `DEPENDS-ON`, and CSV and Linear imports bring them along. Two people can't make tasks wait for each other, even offline. ([Tasks and projects](docs/tasks-and-projects.md#wait-for-another-task)) - **Attachments on tasks and comments.** Add files and links to a task from the new **Attachments** row in the task panel (pick, paste a screenshot or drop files), or attach files while writing a comment. Files added offline are listed at once and upload on the next sync; every device sees them as soon as the server has stored them. Images get thumbnails, task rows show a paperclip with the count, and the installed app takes photos and documents from the phone's share sheet. The browser extension can add a screenshot of the page. Backups and the new **Download export with files (ZIP)** include the files, and importing that ZIP brings them back. Encrypted accounts encrypt the files on the device too. The owner sets a size limit per file and a total per account under **Settings → Administration → Attachments**, and can turn attachments off. ([Attachments](docs/attachments.md)) - **Search and commands on Ctrl+K.** One box (Ctrl/⌘+K, or the magnifying glass in the header) finds any task by title or notes, any project, label, saved view or Settings section, and runs any command by name: go to a view, act on the selected task, use everything in the current project's, view's, label's or section's menu, create things, sync, toggle Vim keys. Filter queries like `@errand & p1` list their tasks right there. It ranks what you pick often first, works offline, and searches only on your device, also for encrypted accounts. ([Search and commands](docs/search.md)) ## [0.4.0] - 2026-10-15 OpenTodo 0.4.0 makes the app yours to look at and easier to move into. Tasks can have a deadline, separate from the day you plan to work on them, and can wait for a start date, in Someday or for This Evening. Pick light, dark or system mode and one of eight colour themes, and set your own date format, clock and first day of the week. Settings are regrouped into clear sections. Import takes Todoist, Things, TickTick and Linear exports as they are, with a switch guide for each. With the assistant on, an **Ask** box and built-in skills (plan my week, triage my inbox, break this down) do common jobs. A setup token can protect a fresh public server, and the assistant's **Test connection** now checks what is on screen. ### Upgrade notes - **Migration `0030`** (how often each assistant skill ran, per user and month; no conversation content) is applied automatically at startup. Back up `/data` before upgrading: there are no down-migrations. No new environment variables are required; `OPENTODO_SETUP_TOKEN` is optional. - **Sync:** new `user_prefs` fields (`date_format`, `time_format`, `week_start`, `theme_mode`, `theme_accent`), the task fields `deadline`, `start` and `evening`, and the reminder kind `deadline` are additive; older clients ignore them. - **Image tags.** The release publishes `ghcr.io/pixari/opentodo:0.4.0` and `:0.4` for `linux/amd64` and `linux/arm64` (`:latest` follows `main`). The Coolify template now follows `:0.4`; if you use `:0.3`, change the tag in **Edit Compose file** and click **Deploy**. - **Binary size** stays at about 23 MB. ### Breaking - **`POST /api/v1/me/assistant/test` no longer answers `409 no_own_provider`.** A member can now test a provider before saving it; with no provider chosen or saved, the route answers `400 invalid_input`. **Migration:** scripts that checked for `no_own_provider` should check for `invalid_input`. ### Changed - **New addresses.** The website and the documentation moved to [open-todo.app](https://open-todo.app) (docs at [open-todo.app/docs](https://open-todo.app/docs/)) and the public demo to [demo.open-todo.app](https://demo.open-todo.app). In-app **Help** links, the generated docs' canonical URLs and the Coolify template now point there. - **Dates and times in your format.** **Settings → Preferences → Date and time** sets the date format (day-month, month-day or ISO), a 12- or 24-hour clock and the first day of the week, once for all your devices; each follows the browser until you choose. Task rows, headings, task details, reminders, comments, history, the Week view and push notifications follow it. Due dates and reminders get new date and time fields that accept what you type in your format (`3/11`, `2026-11-03`, `tomorrow`, `3pm`) and have a calendar you can use from the keyboard. Quick add reads numeric dates in your date format while its own order is automatic, and "next week" starts on your first day of the week. The preferences sync like your other data, work offline and are included in the JSON export. ([Settings](docs/settings.md#date-and-time), [Tasks and projects](docs/tasks-and-projects.md#dates-and-times)) - **Settings: grouped into fourteen sections with a clearer layout.** Settings opens on your profile card and the sections grouped under **You**, **App**, **Data and connections** and, for the owner and admins, **Server**. Security, Encryption, Activity, Preferences, Projects, Backups and Access policies now have their own pages. Every panel is a card that leads with its state (for example whether two-factor authentication is on and how many recovery codes are left), with settings as rows, forms that open from a button, and confirmations that say what will be lost. Old links such as `/settings/general` still work. ([Settings](docs/settings.md)) ### Added - **Start dates, Someday and This Evening.** A task's new **When** row (and Shift+N) defers it until a start date: it leaves Today, Upcoming and the counts, waits in its project's **Later** group, and appears in Today with a **Started** chip on the day. **Someday** (n, or `someday` in quick add) parks a task in the new Someday view (g then o) and clears its due date. **This Evening** (Shift+E, or `tonight` in quick add) lists a task below the day's tasks in Today. The new **Anytime** view (g then n) lists what has no date at all. A due date or deadline that has come always shows a task in Today. Filters gain `someday`, `deferred`, `evening`, `no start` and `start `; recurring tasks move the start date with the due date; CalDAV carries both as `X-OPENTODO-START` and `X-OPENTODO-EVENING`; Things and CSV imports keep them. Sync: two new task fields, `start` and `evening`. ([Tasks and projects](docs/tasks-and-projects.md#when-a-task-starts), [Views](docs/views.md)) - **Deadlines.** A task can have a deadline, the day it must be finished by, separate from the day you plan to work on it. Set it in the task panel's new **Deadline** row, with d on a selected task, or in quick add with `deadline friday` (`Frist`, `échéance`, `scadenza`, `fecha límite`, `prazo` in the other languages). Rows show a countdown (`3d`, `Today`, `2d over`) that turns amber in the last week and red on the day, after it, or when the task is planned after its deadline. Today lists tasks whose deadline is today or past; Upcoming lists undated tasks under their deadline day. Filters gain `deadline <= 3 days` and `no deadline`, reminders can fire before the deadline, recurring tasks move the deadline with the due date, CalDAV serves it as `DUE` (with the due date as `DTSTART`), and imports from Todoist, Things, CSV and JSON keep it. Sync: one new task field, `deadline` (`YYYY-MM-DD`), and a reminder kind `deadline`. ([Tasks and projects](docs/tasks-and-projects.md#deadlines), [Filters](docs/filters.md), [Reminders](docs/reminders.md)) - **Themes and dark mode on demand.** **Settings → Preferences → Appearance** sets the mode (**System**, **Light** or **Dark**) and one of eight colour themes (Emerald, Blue, Indigo, Violet, Rose, Orange, Teal, Graphite) for buttons, links, the focus ring and selection. Both follow your account to every device, also offline, apply at once without a reload, and the app opens in your theme without a flash. Priority, warning and error colours and your project and label colours keep their meaning in every theme, and every theme passes the contrast checks in light and dark. The installed app's title bar follows the theme. Sync: two new `user_prefs` fields, `theme_mode` and `theme_accent`. ([Settings](docs/settings.md#appearance)) - **Assistant skills and the Ask box.** With the assistant on, **Today** and a new **Ask** screen (sidebar, or g then k) show an **Ask OpenTodo** box with example cards, and the assistant panel gains a **Skills** menu: *Turn an idea into a project*, *Plan my week* (within your working day), *Weekly review*, *Triage my inbox* and *Break this down* (also in a task's menu). Each skill uses only the tools it needs, and everything it proposes is reviewed and undoable as before. Pasted notes are kept apart and treated as material, never as instructions. The same skills are served as MCP prompts. While the assistant is off, members see nothing new and the owner one dismissible line. New route `GET /api/v1/assistant/skills`; turns accept an optional `skill` and `workday`. ([AI assistant](docs/assistant.md#skills), [MCP](docs/mcp.md#prompts)) - **Switch from Todoist, Things, TickTick or Linear.** Import accepts each app's own export as it is: a Todoist backup ZIP (or one project's CSV), a copy of the Things 3 database, a TickTick CSV backup and a Linear CSV export. Projects, sections or headings, subtasks and checklists, labels, priorities, due dates (Todoist's in six languages, even without a year), recurring tasks, reminders and completed items carry over; projects that were boards open as boards; Linear issues assigned to you stay yours. What OpenTodo cannot hold yet (comments' authors, estimates in points) is named in the preview. Each app has a switch guide. ([Switch from Todoist](docs/switch/todoist.md), [Things](docs/switch/things.md), [TickTick](docs/switch/ticktick.md), [Linear](docs/switch/linear.md), [Import formats](docs/import.md)) - **Site bundle for the website.** Every release attaches `opentodo-site-bundle-.tar.gz`: the documentation website, product screenshots, the colour tokens as plain CSS, the icon set as an SVG sprite, the changelog as JSON and a manifest (schema 1) with the version, the import formats it supports and a few measured facts. Every CI run uploads the same bundle as an artefact, so the website can preview unreleased docs. Layout version 1 is described in the bundle's `README.md`; paths change only in a release that says so, and stay for one more release. `make site-bundle` builds it locally. Screenshots for the website now live in `docs/screenshots/site/`, and CI skips the Go, cross-compile and layout jobs for documentation-only pull requests. - **Server facts in the site bundle.** The bundle manifest now lists the image platforms, port, data folder, database, licence and export format (`architectures`, `port`, `dataDir`, `database`, `license`, `exportFormat`, `exportFormatVersion`), read from the release workflow, the Dockerfile, the export route and LICENSE, so the website can show them for the version it describes. Its icon sprite also carries the Lucide icons the website uses beyond the app's own (`web/src/ui/icons/site-icons.json`), which the app itself never loads. - **A setup token protects a fresh server.** With `OPENTODO_SETUP_TOKEN` set, creating the first account requires that token, so a public instance cannot be claimed by whoever opens the URL first. The Coolify template sets it to a secret Coolify generates; copy it from the service's Environment Variables into the setup screen. Unset, nothing changes. ([Coolify](docs/self-hosting/coolify.md#first-visit), [Docker](docs/self-hosting/docker.md#docker-compose)) - **`ot mcp`: MCP for assistants that only speak stdio.** The command-line client bridges an assistant's stdio MCP connection to your server's `/mcp` endpoint, so clients without HTTP transport can use OpenTodo too. It refuses a token that can make changes unless you pass `--allow-full-token`, and never weakens `--read-only` or `OPENTODO_READ_ONLY`. Thanks to @adity982 for the contribution. ([AI assistants (MCP)](docs/mcp.md)) - **Icons, menus and a smart picker.** Every emoji and symbol that stood in for an icon is now a line icon from one consistent set (Lucide, bundled, no new download). Menus have icons, groups and shortcut hints and work fully with the keyboard (arrow keys, Home and End, typing a letter). The label and assignee pickers show the labels and people you use most on this device first, and find them by the start of any word or by letters in order, ignoring case and accents (“cafe” finds “Café”). Your pick history stays on the device and is cleared when you sign out. ([Design system](docs/design-system.md#building-blocks)) - **New visual foundation.** Design tokens for colour, type, spacing, radius, elevation and motion in light and dark, the bundled Inter typeface (54 KB, served by your own server, never a font CDN), and refreshed buttons and inputs, keeping OpenTodo's emerald and slate look. Every text colour meets WCAG AA contrast in both themes, checked automatically. Contributors get a component workbench (Storybook, development only) with accessibility and visual-regression checks. ([Design system](docs/design-system.md)) ### Fixed - **Test connection tests what is on screen.** In the assistant settings, **Test connection** used the last saved settings, so a model id typed but not yet saved was reported as "enter a model id first", and a replaced key was never checked. The owner's and the member's tests now use the provider, model, base URL, key and private-endpoint switch on screen, saved or not, with the same checks as saving, and never save anything. Members can test before saving their own provider. A result disappears when you change what it tested. ([AI assistant](docs/assistant.md), [API](docs/api.md)) - **The assistant says what an encrypted account can do.** On an account with end-to-end encryption, the assistant panel, the Ask screen and **Settings → AI → Assistant** now link to **Settings → Encryption**, where encryption can be turned off to use the assistant, instead of only saying it is unavailable. ([AI assistant](docs/assistant.md)) - **Test notifications prove delivery.** **Settings → Notifications → Send test notification** used to show a notification from the page and always say "sent", even when reminders could never reach the device. It now sends a real push through your server and the push service, shows it even while the app is open, and says **Received on this device** only when the device confirms it, otherwise **accepted, not seen** with the operating-system settings to check. A new **Delivery check** lists each step push needs on the device (permission, service worker, subscription, push service, last test) with a fix for each problem. New route `POST /api/v1/push/test` (session or full token, 5 per 10 minutes). ([Notifications](docs/notifications.md#check-that-it-works), [API](docs/api.md#reminders-and-push)) ## [0.3.1-rc.1] - 2026-10-15 A release candidate that tests the release pipeline: the release now attaches `opentodo-site-bundle-v0.3.1-rc.1.tar.gz` (the documentation website, screenshots, colour tokens, icons, changelog and feature manifest for the website) next to the binaries, listed in `SHA256SUMS`. It carries everything under Unreleased above; it is not a supported release, and the `:0.3` and `:latest` image tags do not move. ## [0.3.0] - 2026-10-12 OpenTodo 0.3.0 is about bringing your own AI, if you want one. There is a chat assistant inside the app that answers questions about your tasks and proposes changes you review and undo in one step, with Anthropic, OpenAI, Gemini or a model on your own network. An assistant you already use can now make changes over MCP, and the `ot` command is ready to be driven by coding agents. All of it is off until the owner turns it on; nothing is sent anywhere before that. [Use AI with OpenTodo](docs/ai-and-agents.md) compares the three options. Settings is also split into sections with their own pages, and a project can be shared with a single invite link. ### Upgrade notes - **Migrations `0026` to `0029`** (agent tokens and switches; assistant consent and usage; members' own assistant provider; project invite links) are applied automatically at startup. Back up `/data` before upgrading: there are no down-migrations. No new environment variables are required; `OPENTODO_MCP_WRITES` is optional. - **Image tags.** The release publishes `ghcr.io/pixari/opentodo:0.3.0` and `:0.3` for `linux/amd64` and `linux/arm64` (`:latest` follows `main`). The Coolify template now follows `:0.3`; if you use `:0.2`, change the tag in **Edit Compose file** and click **Deploy**. - **`ot` scripts:** destructive commands now need `--yes` off a terminal, and refusals exit 5 instead of 1 (see Breaking below). - **Binary size.** The server binary grows from about 17 MB to about 23 MB, mostly the official Anthropic SDK used by the assistant. ### Breaking - **`ot`: destructive commands need `--yes` without a terminal.** `ot rm`, `ot project rm`, `ot token revoke` and `ot auth logout` used to proceed silently when stdout was not a terminal or `--json` was given. They now refuse with exit code 2 (`confirmation_required`) before sending anything. **Migration:** add `--yes` to scripts and cron jobs that delete on purpose, e.g. `ot rm "$ID" --yes`. On a terminal without `--json` they still ask. ([CLI](docs/cli.md#migrating-scripts)) - **`ot`: new exit code 5, "not permitted".** Refusals for scope, role, a switch, encryption or read-only mode (`insufficient_scope`, `forbidden`, `not_permitted`, `writes_disabled_by_owner`, `writes_disabled_by_user`, `encryption_enabled`, `read_only`) exit 5 instead of 1, so scripts and agents can stop instead of retrying. **Migration:** scripts that test for exactly `1` should also accept `5`; scripts that test for non-zero are unaffected. The op log and the REST API are unchanged. ### Added - **Invite to a project with a link.** In a project's **Share** dialog, the owner and admins can create a single-use **invite link** with a role (viewer or editor; admin for the owner), an optional email binding and an expiry from 1 hour to 30 days (default 7 days). The link is shown once; pending links are listed with their role, email, expiry and creator and can be revoked. Opening `/invite/p/` shows who invited you to which project: signed in, you select **Join project**; otherwise you sign in and join, or create an account that joins in the same step. A link creates accounts only when its creator is the server owner or an admin, when open sign-up is on, or when the owner switches on **Members can invite new people with project links** in **Settings → Administration** (off by default); otherwise the page asks the person to get an account first. Links stop working when revoked, used or expired, when the project is deleted, or when their creator is no longer the project's owner or an admin. Links are server-side data, never synced; refused on the demo instance. ([Sharing](docs/sharing.md#invite-with-a-link), [API](docs/api.md#project-invite-links)) - New routes: `GET/POST /api/v1/projects/{id}/invites`, `DELETE /api/v1/projects/{id}/invites/{inviteId}`, `GET /api/v1/project-invites/{token}` (public preview), `POST /api/v1/project-invites/{token}/accept`, `GET/PUT /api/v1/settings/project-invites`; `POST /api/v1/auth/register` accepts `projectInvite`. New error code `project_invite_not_allowed`; audit events `project_invite.created`, `project_invite.revoked`, `project_invite.accepted`. Migration `0029_project_invites.sql` is applied automatically. - **Your own assistant provider.** When the owner lets members bring their own key, each person can choose Anthropic, OpenAI or Google Gemini, a model and their own key in **Settings → AI → Assistant → Which assistant you use**, with **Test connection**. Members are never offered the compatible endpoint, so they cannot make the server connect to an address of their choosing. The disclosure names the provider actually used ("from this server to … with your own API key") and is asked again when it changes. Turning member keys off makes everyone use the owner's provider again; choices are kept for later. Keys stay in `/secrets/assistant.json`, never returned, logged, exported or backed up. ([AI assistant](docs/assistant.md#use-your-own-provider-and-key)) - **Own keys outside the allowance.** Answers on a member's own key are recorded and shown as "your key", but neither counted nor stopped by the monthly token allowance unless the owner turns on the new **Count members' own keys toward the monthly limit** (`cap_member_keys`). One answer per person and 8 per server still apply. The owner sees how many people use their own provider and key. - API (additive): `own_provider`, `own_model` and new status fields on `GET/PUT /api/v1/me/assistant`; `POST /api/v1/me/assistant/test` (signed-in sessions, 5 per 10 minutes); `cap_member_keys` and `own_key_members` on `/api/v1/settings/assistant`; `counted_tokens` and `own_key` on the usage route and `own_key` on the turn's `usage` event. ([API](docs/api.md#in-app-assistant)) - **AI assistant in the app.** A chat panel (Shift+I or **Assistant** in the header; a side sheet on wide screens, full screen on phones) that answers questions about your tasks and *proposes* changes: "move everything overdue to Friday", "break this task into steps". Each proposal is a reviewable list with every change in words; you apply the checked part (at most 50 changes) and undo it in one step. Applied changes are ordinary edits from your device: offline-capable, synced, labelled **Assistant** in history. Providers, each with the owner's or a member's own key: Anthropic through the official Anthropic Go SDK (default `claude-opus-5-5`; `claude-sonnet-5-5` and `claude-haiku-5-5` offered), OpenAI, Google Gemini and any OpenAI-compatible endpoint, including Ollama or LM Studio on your network. Off by default: the owner enables it in **Settings → Assistant** (provider, models, keys, private endpoints, monthly token allowance, member keys, **Test connection**); each person accepts a disclosure naming the provider and the fields sent before their first message, and again when the provider changes. Keys live only in `/secrets/assistant.json`, never returned, logged, exported or backed up. The assistant cannot change sharing, members, settings or tokens, make outbound requests or delete permanently; task text is marked as untrusted. Conversations stay on the device and are deleted at sign-out. Not available for end-to-end encrypted accounts or on the demo instance. Optional per-device auto-apply of up to 3 small, reversible changes, off by default. ([AI assistant](docs/assistant.md), [API](docs/api.md#in-app-assistant), [security](SECURITY.md#in-app-assistant)) - New routes: `GET/PUT /api/v1/settings/assistant`, `POST /api/v1/settings/assistant/test` (owner), `GET/PUT /api/v1/me/assistant`, `GET /api/v1/me/assistant/usage`, and `POST /api/v1/assistant/turn` (signed-in sessions only; streams server-sent events). `/api/v1/sync` accepts the op device prefix `assistant:`; history entries from it carry `device_label: "Assistant"`. - Dependency: `github.com/anthropics/anthropic-sdk-go` (the server binary grows by about 5 MB; see the change's design notes). - **`ot` for agents and automation** ([CLI](docs/cli.md#using-ot-from-an-agent)): `--json` output is documented with JSON Schemas and `output_version` 1 (`ot version --json`); `ot schema` (or `ot help --json`) prints every command, flag, exit code, error code and output schema, offline; `--dry-run` on every write command resolves references and prints the requests it would send without writing; `--idempotency-key` on `ot add` and `ot project add` makes retries return the same entity; `ot done`/`ot reopen` on a task already in that state report `unchanged` and send nothing, and `ot edit` with unchanged values sends nothing; `--read-only` (or the optional `OPENTODO_READ_ONLY=1`) refuses every write before any request; `ot` refuses writes locally for `read` tokens and outside the allowed routes for `agent` tokens; `ot auth status` shows the token's scope and whether assistant writes are allowed. With `--json`, errors carry an additive `exit` field and stderr gets one `ot:` line; `done`/`reopen`/`rm` add `ref` and `status` to each task. - **Agent skill file.** `ot skill` prints `SKILL.md`, which teaches an agent to use `ot` safely (agent token, `--json`, dry runs, idempotency keys, exit codes, task text as untrusted data). Each release ships it as `opentodo-agent-skill.zip`. - **`Idempotency-Key` on `POST /api/v1/{collection}`**: the entity and op ids derive from your account, the collection and the key, so a retried create returns 200 with the stored entity and `Idempotent-Replayed: true`; the same key with other content answers 409 `idempotency_conflict`. Keys never cross accounts and need no storage. ([API](docs/api.md#idempotency-keys)) - **`agent` tokens on REST content routes**: creating tasks, projects, labels and comments, editing tasks, renaming or recolouring projects and trashing the token's own tasks, under the same two switches, limits, `agent.write` audit, history label and encryption rules as the MCP write tools (they run through the same tool registry). Every other REST write still refuses them. ([API](docs/api.md#agent-tokens)) - `GET /api/v1/me` gains an additive `auth` object: `method`, `scope`, `token_id`, `token_name` and, for `agent` tokens, `writes_allowed`/`writes_denied`. Never the secret. ([API](docs/api.md#credential-introspection)) - **AI assistants can make changes, when you allow it.** The MCP endpoint gains ten write tools: `create_tasks`, `update_tasks`, `complete_tasks`, `reopen_tasks`, `move_tasks`, `schedule_tasks`, `create_project`, `create_label`, `add_comment` and `trash_tasks`. They appear only when the owner turns on **AI assistants may make changes**, you turn on **Allow AI assistants to make changes to my tasks** (both off by default, in Settings → AI assistants), and the assistant uses a new **`agent`** token. Full-access and read-only tokens keep read-only tools. Every change goes through the op log, syncs and converges like any edit, shows in history as "AI assistant (*token name*)", is recorded in the audit log as `agent.write` (no content), and can be undone. Nothing is ever deleted permanently; `trash_tasks` only accepts tasks the same token created; assistant comments notify no one. Limits: 50 items per call, 30 write calls per minute and 1,000 changed items per day per token. For end-to-end encrypted accounts, tools that would write readable text are not offered (`encryption_enabled`); scheduling, completing and moving still work. ([AI assistants](docs/mcp.md#letting-an-assistant-make-changes)) - `agent` token scope: refused on every REST write like `read`, except the agent content routes above (it cannot create tokens, change settings or sync raw operations). Create one in Settings → AI assistants, Settings → API tokens, or with `ot token create --scope agent`; `ot token list` shows each token's scope. - `GET/PUT /api/v1/me/agent-access` (your own switch; sessions and full tokens only) and `writes_enabled` on `/api/v1/settings/mcp`. The optional `OPENTODO_MCP_WRITES=true` presets the owner's switch on first start. - New internal package `internal/agenttools`: one registry of agent tools (definitions, validation, planning, limits, permission gate), to be shared with the planned in-app assistant. - **Coolify one-click template, ready to submit upstream.** `deploy/coolify/opentodo.yaml` now follows Coolify's current template conventions: `SERVICE_URL_OPENTODO_8080`, the `0.2` image line, a `/data` volume and a `/opentodo healthcheck` health check. It also lists the useful optional variables with their defaults (including `OPENTODO_TRUSTED_PROXIES` and email to task), because Coolify passes a variable from its UI only when the compose file references it. An explicit `OPENTODO_BASE_URL` wins over Coolify's generated URL. `deploy/coolify/upstream/` mirrors Coolify's repository paths and holds a draft docs page, the pull request text and step-by-step submission notes. Until the template is listed in Coolify, paste it as a **Docker Compose** resource. ([Coolify](docs/self-hosting/coolify.md#one-click-template-not-yet-in-coolifys-service-list)) - `scripts/coolify-template-check.mjs` (in `make check` and CI) fails when the template breaks those conventions, names an `OPENTODO_*` variable the server does not read, lags behind the latest release, or differs from its upstream copy. `make coolify-smoke` starts the image the way Coolify does and checks the health check, the owner setup, closed sign-up, the https base URL and data surviving a redeploy. ### Changed - **An invalid `OPENTODO_TRUSTED_PROXIES` entry no longer stops the server.** A typo such as `cloudflar` used to fail startup, so the health check failed and Coolify rolled the deploy back with the cause only in the container log. Now the server starts, ignores the invalid entries (keeping the valid ones; with none valid it behaves as unset, so trust is never widened), logs one error line naming them with a "did you mean cloudflare?" hint, and shows the owner a **Server configuration warning** at the top of **Settings** and **Settings → Administration**. Entries may also be separated by spaces. `GET /api/v1/me` gains an additive `config_warnings` array for the owner's session only. Every other invalid variable still stops startup. ([Behind a reverse proxy](docs/self-hosting/docker.md#behind-a-reverse-proxy)) - **Settings has sections.** Instead of one long page, `/settings` lists the sections with a line on what each holds, and each opens on its own page: **Account**, **General**, **Notifications**, **Data**, **Capture**, **Integrations**, **AI** and, for the owner and admins, **Administration** (`/settings/account`, `/settings/general`, …). Wide screens keep the section list beside the content; phones get a back link. Every setting works as before, Help links open the same guides, and `/settings/import` is unchanged. The user guide now names the section, for example **Settings → AI → AI assistants**. The single sign-on link flow returns to `/settings/account`. ([Settings](docs/settings.md)) - MCP read results mark text people wrote: titles, descriptions, names and comment bodies now sit inside an `untrusted_text` object, every result carries a `_notice` telling the assistant to treat that text as data, tasks of shared projects carry `shared: true` and comments `author_is_you`. Label ids are listed as `label_ids` next to the names. The tool definitions themselves are unchanged. ([AI assistants](docs/mcp.md#read-tools)) - `/api/v1/sync` refuses operations from devices whose id starts with `agent:` or `caldav:` (400 `invalid_op`): those ids are reserved for the server's own writers. - API requests with a token scope this server does not know are refused on every write route (previously only `read` was). - **Migration `0026`** adds two side tables (`api_token_agent`, `user_agent_access`) and is applied automatically at startup. Rolling back to an older version is safe: `agent` tokens then act as read-only tokens. - Images built from source by Coolify report `dev+` instead of `dev` when the application's **Source commit availability** is *Available during build* (the Dockerfile reads `SOURCE_COMMIT`). - Coolify guide: which resource types roll and which recreate, why plain **Restart** and API deploys keep a cached image, first-visit owner setup, domains and passkeys, Cloudflare (proxy and Tunnel, Browser Cache TTL), and Coolify volume backups. ### Fixed - A reminder that became due while a dialog, menu or popover was open took the keyboard focus out of it. It is still announced, but focus now stays where you are. - Phones could keep running the previous release after an update when a CDN such as Cloudflare sat in front: it cached the service worker script for hours. Service worker scripts are now sent with `Cache-Control: no-store` (and `CDN-Cache-Control: no-store`). ## [0.2.0] - 2026-10-11 OpenTodo 0.2.0 is about trying it, using it on a phone and running it without fuss. Every visitor to the public demo now gets a private sandbox. The phone layout has been reworked so menus, pickers and dialogs fit small screens. There is a docs website and a Help view inside the app, which works offline. Archived projects can be restored with one click. Updates roll out without a Stop → Deploy. Rate limits work correctly behind trusted proxies and Cloudflare. The app meets WCAG AA contrast in light and dark, and this release includes several security fixes. ### Upgrade notes - **From 0.1.0, stop once.** 0.1.0 locked the data directory exclusively, so a 0.2.0 container cannot start beside it. Plain `docker compose pull && docker compose up -d` works as before. With Coolify or another orchestrator that starts the new container first, click **Stop**, then **Deploy** for this one upgrade (otherwise the deploy fails with "data directory is in use by another opentodo process" and rolls back). From 0.2.0 on, updates roll without downtime. ([Docker](docs/self-hosting/docker.md#updating), [Coolify](docs/self-hosting/coolify.md#updating)) - **Image tags.** The release publishes `ghcr.io/pixari/opentodo:0.2.0` and `:0.2` for `linux/amd64` and `linux/arm64` (`:latest` follows `main`). The Coolify template in `deploy/coolify/opentodo.yaml` now follows `:0.2`; if you pinned `:0.1`, change it. - **Migration `0025`** (demo sandbox bookkeeping) is applied automatically at startup. Back up `/data` before upgrading: there are no down-migrations. No new mandatory environment variables; `OPENTODO_TRUSTED_PROXIES`, `OPENTODO_DEMO_MAX_SANDBOXES` and `OPENTODO_DEMO_MAX_DB_MB` are optional. - **Behind a CDN or second proxy**, set `OPENTODO_TRUSTED_PROXIES` (for example `cloudflare`) so rate limits and the audit log see visitors' addresses. ([Reverse proxy](docs/self-hosting/docker.md#behind-a-reverse-proxy)) ### Added - **User documentation**: seventeen task-based guides for people using the app (getting started, installing the app on iOS, Android and desktop, tasks and projects, views, sections and boards, labels and filters, recurring tasks and reminders, time blocking, sharing and comments, notifications, offline and sync, keyboard shortcuts, account security, import and export, trash and history, settings, troubleshooting and FAQ), with light and dark screenshots generated from the demo data (`make docs-screenshots`). Every page in `docs/` has front matter (title, summary, audience, the capabilities it covers, the app screens it explains). ([Docs](docs/README.md)) - **Docs website** at [opentodo.pixari.dev/docs](https://opentodo.pixari.dev/docs/), generated from `docs/` by `make docs` into `website/docs/`: grouped by audience, in-browser search over a prebuilt index (nothing typed leaves the browser), light and dark, readable without JavaScript, "On this page", previous and next, the version described and "Edit this page on GitHub". "Docs" is in the website's navigation. - **Help inside the app**: a Help view with the user guide of the running version, searchable and available offline (a lazily loaded chunk, precached by the service worker; screenshots stay on the website). Open it from the sidebar, Settings, the `?` overlay, `g` then `?`, or `:help ` with Vim keybindings. Settings sections, the share dialog, the Filter view, quick add and the import screen have a Help link that opens the matching section in a side panel (a full-screen sheet on phones) without closing what you were doing. - **Unarchive projects**: Settings → Projects lists your archived projects, with their parent and open task count, and a **Restore** button that brings one back to the sidebar. Restoring is an ordinary change, so it works offline and syncs to every device; sub-projects stay archived until restored on their own, a project restored by hand is not archived automatically again on that device, and in shared projects only the owner or an admin can restore. Previously only the automatic-archive notice could undo an archive. ([Trash and history](docs/trash-and-history.md#bring-back-an-archived-project)) - `npm run docs:check` (part of `make check` and CI) fails on missing front matter, broken links or anchors, images without alt text, capabilities in `openspec/specs/` that no page covers, help links to unknown sections, a keyboard guide that differs from the app, and stale generated files. The shortcut table moved to `web/src/lib/shortcuts.ts`, shared by the `?` overlay and the keyboard guide. The CI layout job also runs axe-core on the Help view, the help panel and the docs website, and checks the docs website at 360, 390 and 1280 px with and without JavaScript. ### Changed - The public demo (`OPENTODO_DEMO=true`) gives every visitor a **private sandbox** instead of one shared account: "Try the demo" creates a password-less account with its own copy of the sample data and a non-interactive collaborator, and nobody sees anybody else's changes. Sandboxes are deleted after 2 hours of inactivity, 24 hours after creation and by the nightly reset, which now also runs `VACUUM`. The published demo password is gone; the first start deletes the old shared demo accounts. `auth/status.demo` carries a creation token instead of credentials; new route `POST /api/v1/demo/sandbox`, new error codes `demo_expired`, `demo_limit`, `demo_busy` and `demo_token`; optional `OPENTODO_DEMO_MAX_SANDBOXES` and `OPENTODO_DEMO_MAX_DB_MB`. Migration `0025` adds the sandbox bookkeeping tables. ([Public demo](docs/self-hosting/demo.md)) - Links in comments and notes render with `rel="nofollow ugc noopener noreferrer"` on every instance. ### Security - Demo instances bound abuse: sandbox creation needs a single-use, signed, 10-minute token from the sign-in page and a same-origin request, at most 5 new sandboxes per address per hour (addresses are stored only as salted hashes), a live cap with idle eviction and a database size guard; each sandbox may write 600 ops per hour, 5,000 ops and 2 MB of values in total, values are capped at 4 KB (32 KB for notes), and above the database size limit live sandboxes cannot write either. Every demo response carries `X-Robots-Tag: noindex, nofollow` and the demo page declares `noindex`. - Per-address rate limits (sign-in, passkeys, two-factor, single sign-on, CalDAV, demo sandboxes) and audit-log addresses now use the **last** `X-Forwarded-For` hop, the one the reverse proxy appended, instead of the first entry, which the client controls behind an appending proxy such as Traefik and could change to evade the limits. Run one proxy in front of the server and do not publish its port. ([Reverse proxy](docs/self-hosting/docker.md#behind-a-reverse-proxy)) - Behind a CDN, set the optional `OPENTODO_TRUSTED_PROXIES` (IP addresses, CIDR prefixes and/or `cloudflare`) so the CDN's hop is skipped and visitors are not all counted as the CDN's address. With `cloudflare`, requests from a Cloudflare edge use `CF-Connecting-IP`. ([Reverse proxy](docs/self-hosting/docker.md#behind-a-reverse-proxy)) ### Fixed - Phones: every menu, popover, picker and dialog now fits the screen. The notifications panel was cut off on the left; it, the mention picker and the section, project, label and view menus open as bottom sheets below 640 px and next to their button (kept inside the screen) above. Dialogs (rename, share, sign-in confirmation, user administration, voice consent, label and assignee pickers, keyboard help) are full screen on phones with a pinned close button, and all overlays close with Escape or a tap outside and return focus to their button. Touch targets are at least 44 px, row menus are visible without hover on touch screens, long words and URLs wrap, the sidebar scrolls on short screens, and layouts respect the safe areas and the on-screen keyboard. A new CI job opens every screen and overlay at 360, 390 and 768 px in Chromium and WebKit (`make layout-check`). ([mobile-layout](openspec/changes/archive/2026-10-11-mobile-layout/proposal.md)) - **Contrast and accessibility** across the app: secondary text (sidebar section names, counts, dates, descriptions), the "All" links, primary buttons (now a darker green, white text at 5.5:1), status and priority colours and dark-mode muted text all meet WCAG AA (4.5:1) in light and dark. Secondary text uses two theme-aware tokens, `text-muted` and `text-faint`. Label and assignee pickers no longer nest buttons inside list options, the priority buttons say which one is selected (`aria-pressed`), and the filter and view result lists are named regions. The layout check now runs axe-core on every screen and overlay in light and dark, not only on the Help view, and fails on serious or critical issues. - Task and project **History** names people, projects, sections and parent tasks instead of showing their ids, labels fields in words ("assignee" rather than `assignee_id`), and no longer lists writes that kept the value, such as the plain-text copy of a note rewritten when the note is upgraded. - Zero-downtime updates work again: several server processes may now use one data directory, so Coolify's rolling update (new container started before the old one stops) no longer fails with "data directory is in use by another opentodo process" and rolls back. Background jobs (reminder push, webhooks, backups, email polling, Trash cleanup, audit pruning, demo reset) run in one process only and move to the new one when the old one stops; maintenance commands still require every server to be stopped. Upgrading from 0.1.0 needs one stop-then-deploy. ([Updating](docs/self-hosting/docker.md#updating)) ## [0.1.0] - 2026-10-11 The first release, and the first feature-complete version: every item on the [roadmap](ROADMAP.md) is implemented. Each feature was planned and built as an OpenSpec change; the archived changes under `openspec/changes/archive/` hold the full proposals, designs and task lists, and `openspec/specs/` the resulting behaviour contracts. ### Upgrade notes - **No mandatory configuration.** Every new environment variable is optional and every feature that talks to the outside world is off until configured. The complete list is in the [README](README.md#configuration). - **Database migrations** `0001` to `0024` are embedded and applied automatically at startup, in order, tracked by `PRAGMA user_version` (numbers `0016`–`0018` and `0020` are unused). Back up `/data` before upgrading an existing instance: there are no down-migrations. Rolling back to a binary older than user administration lets deactivated accounts sign in again. - **Update the server before clients.** The op schema only grew (new fields and entities, nothing renamed), but an older server rejects sync batches that carry fields it does not know; those ops wait in the device's outbox until the server is upgraded. Older clients keep syncing their personal data and skip ops for entities they do not know. - **Sizes** (measured for this release, `linux/amd64`): server binary 16,715,938 bytes (15.9 MiB) built with `-trimpath -ldflags="-s -w"`, web app embedded; `ot` CLI about 6.9 MB; initial web bundle 261 KB JS + 10.7 KB CSS gzipped, with the rich-text editor (97 KB), voice recognition (4 KB) and Vim layer (4 KB) loaded on demand; idle memory about 18 MB on a fresh instance. The container image adds the distroless static base (about 2 MB) to the binary. Release assets: server and `ot` binaries for Linux, macOS and Windows (amd64 and arm64), the browser extension zip and `SHA256SUMS`; images `ghcr.io/pixari/opentodo:0.1.0` and `:0.1` for `linux/amd64` and `linux/arm64`. ### Added **Core platform** - Go + SQLite server with the React PWA embedded, local-first storage in IndexedDB, field-level last-writer-wins op log, owner setup, sessions, API tokens, REST API that writes through the op log, JSON export, a single distroless container and a Coolify-ready compose file. [Architecture](docs/architecture.md), [sync protocol](docs/sync.md) - Live sync: a server-sent event stream (`GET /api/v1/sync/events`) wakes other devices as soon as ops are recorded; 30 s polling remains the fallback. [Live updates](docs/sync.md#live-updates), [proxy notes](docs/self-hosting/docker.md#live-updates-through-the-proxy) - Vim keybindings, opt-in per device and loaded only when enabled. **Capture** - Natural-language quick add in English, German, French, Italian, Spanish and Portuguese: dates, times, `#project`, `@label`, `p1`–`p4`, `+30m` estimates, with preview chips; parsed on the device. [Quick add](docs/quick-add.md) - Voice capture into quick add (`Shift+V`), opt-in per device, using the browser's speech recognition. [Voice](docs/quick-add.md#voice) - Share target, bookmarklet and a Manifest V3 browser extension for Chromium and Firefox (`make extension`), opening a capture screen at `/share`. [Capture](docs/capture.md) - Email to task: a personal secret address per account, polled from one operator-configured IMAP mailbox (`OPENTODO_EMAIL_*`). [Email](docs/email.md) - `ot` command-line client (`cmd/ot`), cross-compiled for Linux, macOS and Windows and attached to releases. [CLI](docs/cli.md) **Organisation and views** - Nested projects and subtask trees to any depth, with deterministic, write-free cycle breaking for concurrent moves. - Sections and a Kanban board per project; new `section` entity and task field `section_id`. [Sections](docs/api.md#sections) - Manual ordering by drag and drop and `Alt+↑/↓`, one `position` op per move. [Manual order](docs/sync.md#manual-order) - Labels as a synced entity referenced by id from `task.labels`. [Task labels](docs/sync.md#task-labels) - Filter query language evaluated on the device, and saved views and dashboards (`view` entity, REST `/api/v1/views`, validated by a Go port of the parser). [Filters](docs/filters.md) - Rich-text notes with collaborative editing: new task field `notes_doc` (a Yjs document envelope, up to 256 KB) next to `description`, which stays the note's Markdown projection for the API, export, CalDAV, MCP and webhooks. [Mergeable documents](docs/sync.md#mergeable-documents) - Trash with restore, revision history from the op log, permanent purge (`purges` table, `purged` in sync responses), automatic purge after `OPENTODO_TRASH_RETENTION_DAYS` (default 30) and automatic archiving of finished projects. [Trash and history](docs/api.md#trash-and-history), [Purge](docs/sync.md#purge) **Time** - Duration estimates (task field `estimate_minutes`) and Day/Week time-blocking calendars with drag, resize and full keyboard control. - Recurring tasks: task fields `recurrence` and `series_id`, one grammar implemented in Go and TypeScript against 180 shared vectors, convergent completion of the same occurrence on two offline devices. [Recurrence](docs/recurrence.md) - Reminders (`reminder` entity, relative or absolute) with in-app notifications, snooze and dismiss, and web push implemented with the standard library (VAPID, RFC 8291); key pair in `/push_vapid.pem`, `OPENTODO_PUSH=off` to disable. [Reminders](docs/reminders.md) - Time-zone-fixed due times: optional `tz` key in `due` (IANA zone, only with `time`), resolved by `internal/zoned` and the web app against shared vectors. [Zoned due values](docs/sync.md#zoned-due-values) **Collaboration** - Project sharing with viewer, editor, admin and owner roles: the op log is partitioned into sharing scopes, ops are authorised against the author's membership at receipt and refused ones are reported per op. [Sharing scopes](docs/sync.md#sharing-scopes), [API](docs/api.md#sharing) - Task assignment (`assignee_id`), the Assigned to me view and in-app notifications (`GET /api/v1/notifications`). [Assignment](docs/api.md#assignment-and-notifications) - Threaded Markdown comments (`comment` entity) with `@mentions`, and an activity log derived from the op log, per task and per project. [Comments](docs/api.md#comments-and-activity) - Activity feed across shared projects with unread counts (`project_prefs` entity, personal scope only; `GET /api/v1/activity`). [Activity feed](docs/api.md#activity-feed) - Invitations: single-use, expiring links in Settings → Members. [Invitations](docs/api.md#invitations) - User administration: admin role, deactivation, deletion, password reset links, owner transfer, own password change and `opentodo reset-owner-password` on the host. [User administration](docs/api.md#user-administration) **Integrations and data** - Data import of the app's own export, CSV and JSON exports of other task managers, with preview, duplicate detection and undo (`OPENTODO_IMPORT_MAX_MB`, default 20). [Import](docs/import.md) - Scheduled backups to a folder, S3-compatible storage or WebDAV with retention, optional `OTBK1` encryption, `OPENTODO_BACKUP_*` presets and `opentodo restore`; sync responses carry a log `generation` so devices re-send newer edits after a restore. [Backups](docs/backups.md) - Signed outbound webhooks with a durable queue, retries, delivery log and SSRF protection (`OPENTODO_WEBHOOKS_ALLOW_PRIVATE` preset). [Webhooks](docs/webhooks.md) - CalDAV two-way sync under `/dav/` with per-device app passwords (`otap_…`), off until the owner enables it (`OPENTODO_CALDAV` preset); new task field `ical_extra` keeps unmapped iCalendar properties. [CalDAV](docs/caldav.md) - Read-only MCP endpoint at `/mcp` for AI assistants (five tools, `opentodo://` resources), off until the owner enables it (`OPENTODO_MCP` preset). [MCP](docs/mcp.md) **Security and access** - Passkeys (WebAuthn) when `OPENTODO_BASE_URL` is https; verification in `internal/webauthn`. - Two-factor authentication (TOTP) with recovery codes, remembered browsers, an owner policy (`none`, `owner`, `all`), step-up re-authentication for sensitive routes and `opentodo mfa-reset` on the host. - Single sign-on with one OpenID Connect provider (PKCE, JWKS verification with the standard library), optional `OPENTODO_OIDC_*` overrides, account linking and an SSO-only mode with owner break-glass. [SSO](docs/self-hosting/sso.md) - Audit log of security events with retention, NDJSON export and opt-in client addresses. [What is logged](SECURITY.md#what-is-logged), [API](docs/api.md#audit-log) - Optional end-to-end encryption of task content per account (Web Crypto, AES-256-GCM, passphrase plus recovery key); routes under `/api/v1/encryption/`. [E2EE](docs/e2ee.md) **Launch** - Demo mode for a public demo instance: `OPENTODO_DEMO=true` creates a hidden owner and a shared demo account shown on the sign-in screen ("Try the demo"), seeds realistic data through the op log, wipes and reseeds it every night at 03:00 UTC (devices recover through purge markers, sessions stay), shows a banner, and refuses sign-up, account changes, integrations, import and encryption with 403 `demo_restricted`; no outbound requests. [Demo instance](docs/self-hosting/demo.md) ### Changed - Deleting a project only tombstones the project; its tasks keep their `project_id` and come back when the project is restored from the Trash. - API tokens have a scope: `read` tokens are refused on every write route and on `/api/v1/sync` (`403 insufficient_scope`); existing tokens stay `full`. - `/api/v1/sync` responses gained additive fields: `rejected`, `memberships`, `purged`, `trashRetentionDays` and `generation`; requests may carry `tzOffset`. Served ops carry `actor`. Older clients ignore them. - REST collections and the export include projects shared with you (with read-only `role` and `owner_id`), and the export gained `labels`, `views`, `reminders`, `sections`, `comments` and the `encryption` key record. The export format version stays 1. - `ot` embeds Go's time zone database so zoned due times resolve on hosts without one (Windows, distroless). - Server dependencies: `github.com/fxamacker/cbor/v2` (passkeys). Web dependencies: `qrcode-generator` (two-factor QR codes), `yjs` and the `prosemirror-*` packages (rich-text notes, lazy-loaded). Everything else uses the standard library and existing packages. ### Fixed - Imports no longer drop sections, recurrence and reminders. An own export now round-trips its sections, reminders and saved views with their ids (so section placement, recurring series and reminder links stay intact) and every task field, with recurrence stored canonical; reminders whose time has passed undelivered are imported dismissed so no stale notifications go out. Comments are reported in the preview but not imported (a comment keeps its author), and assignees other than you are dropped with a warning. CSV and the JSON formats of popular commercial task apps map recurrence strings (rule grammar, RRULEs, common spellings, structured objects; anything else is a preview warning), reminders (absolute and minutes before the due time) and sections. The `sections` import option gained `section`, now the default (`label` keeps the old behaviour); the preview counts new sections, recurring tasks and reminders; undo also removes imported sections, reminders and views. [Import](docs/import.md) - Reminders on tasks in a project in the Trash (or under a sub-project whose ancestor is in the Trash) no longer fire, neither as web push nor in the app. Restoring the project brings them back: a reminder whose time passed while the project was trashed is delivered once, late, like after server downtime. [Reminders](docs/reminders.md#trash) - `/api/v1/sync` pull pages, including the history replay for a newly joined shared project, are capped at about 4 MB of op values (always at least one op), so a batch of large rich-text notes no longer produces one huge response; `hasMore` asks the device to continue. [Sync](docs/sync.md#paging) - Purging a task, a project (with its tasks) or emptying the Trash by retention now removes the task's CalDAV resource name in the same transaction; migration `0024` removes rows earlier purges left behind. [CalDAV](docs/caldav.md) - A large import sent one webhook delivery per task. Imports and their undo now produce one `import.completed` or `import.undone` delivery per subscribed webhook with counts and project ids; ops written by an import no longer emit `task.*`/`project.*` events, so a webhook that wants imports subscribes to the new events. Ordinary writes keep their per-event deliveries. [Webhooks](docs/webhooks.md#imports) - Opening the app at a different host than `OPENTODO_BASE_URL` made passkeys fail with the browser's "origin is not authorized for the provided RP ID"; the app now explains which two hosts differ and what to change, and Settings → Security shows the host passkeys are tied to (also `passkeyDomain` in `GET /api/v1/auth/status`). In `docker-compose.yml` an `OPENTODO_BASE_URL` you set yourself now wins over Coolify's generated `SERVICE_URL_OPENTODO`. - Dropdown menus (view, section, project and label menus) could open mostly off screen on phones when the header wrapped; they now shift back inside the screen when opened. - The keyboard-shortcut help could not be scrolled or closed on phones (the panel grew past the screen on iOS Safari). It is now a full-screen sheet on small screens with a pinned close button, the overlay scrolls instead of the panel, and every other dialog overlay scrolls when its content is taller than the screen. - Clients skip ops for entities they do not know instead of failing the whole batch, so an installed PWA that has not refreshed yet keeps syncing when the server gains a new entity. - A reference to a deleted, foreign or not-yet-received section or label is displayed as "No section" or hidden without writing a repair op, so arrival order can never overwrite a newer edit. ### Security - Sign-up is closed after the owner account; new people join by single-use invitation links or, if allowed, single sign-on. - Sensitive routes (new API tokens, app passwords, invitations, user administration, two-factor and SSO settings, encryption key changes) require a sign-in or re-authentication within ten minutes on cookie sessions (`403 reauth_required`). - Rate limits on sign-in, registration, passkeys, the second sign-in step, re-authentication, invitation and reset previews, single sign-on, comment creation, admin actions and failed `/mcp` and CalDAV authentications. - Webhook delivery refuses loopback, private, link-local, metadata and other internal targets unless the owner allows private networks, and never follows redirects. - CalDAV accepts only app passwords; `/api/v1` refuses them. `/mcp` accepts only bearer tokens and checks `Origin`. - Credentials, secrets and audit details are kept out of logs and the audit log by an allow-list; secrets for backups and single sign-on live in `/secrets/` (mode 0600), outside the database and backups. - See [SECURITY.md](SECURITY.md) for the full model. ### Known limitations and verification follow-ups These features are implemented and covered by automated tests (unit, integration and in-process protocol tests), but could not be verified against the real thing in this environment: - **CalDAV** with real clients: Apple Reminders, Thunderbird and DAVx⁵ (Android tasks apps). - **Single sign-on** against a real OpenID Connect provider such as Authentik or Keycloak (tested against an in-process provider). - **Email to task** against real mail providers' IMAP servers (tested against an in-process IMAP server). - **MCP** with real AI assistant clients. - **Voice capture** with a real microphone in Chrome and Safari. - **Web push** on real devices and push services. - **Passkeys** on real devices and authenticators. - **Share target** behaviour in Firefox (and other platforms listed in [docs/capture.md](docs/capture.md)). - **Touch drag and drop** (board, manual ordering, calendar) on real touch devices. Other known limitations: CalDAV does not translate alarms into reminders (alarms survive round trips in `ical_extra`), MCP has no stdio bridge, comments are Markdown text rather than rich text, the browser extension is not published in the stores, and quick add does not parse recurrence or reminders. See the [roadmap](ROADMAP.md#future-ideas). ### Migrations | File | Adds | | --- | --- | | `0001_init.sql` | `users`, `sessions`, `api_tokens`, `ops` (the op log), `fields` | | `0002_invitations.sql` | `invitations` | | `0003_webhooks.sql` | `webhooks`, `webhook_deliveries`, the shared `settings` table | | `0004_passkeys.sql` | `passkeys` | | `0005_data_import.sql` | `import_jobs`, `import_items` | | `0006_audit_log.sql` | `audit_events` | | `0007_backups.sql` | `backup_runs`, the log generation | | `0008_project_sharing.sql` | `op_scopes`, `scope_fields` (replacing `fields`), `project_scopes`, `project_members`, `member_backfills` | | `0009_mfa.sql` | `mfa_totp`, `mfa_recovery_codes`, `trusted_devices`, `session_auth` | | `0010_reminders.sql` | `push_subscriptions`, a scheduler index | | `0011_task_assignment.sql` | `notifications` | | `0012_user_management.sql` | `user_status`, `password_resets`, the single-owner index and guard triggers | | `0013_trash.sql` | `purges` | | `0014_oidc.sql` | `identities`, `session_oidc` | | `0015_comments.sql` | a partial index for comments | | `0019_caldav.sql` | `app_passwords`, `caldav_resources` | | `0021_mcp.sql` | `api_token_scopes` | | `0022_email.sql` | `email_inboxes`, `email_receipts` | | `0023_e2ee.sql` | `encryption_keys` (the wrapped key record) | | `0024_caldav_orphans.sql` | nothing new: removes `caldav_resources` rows of tasks that were already purged | [Unreleased]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.15.0...HEAD [0.15.0]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.14.2...v0.15.0 [0.14.2]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.14.1...v0.14.2 [0.14.1]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.14.0...v0.14.1 [0.14.0]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.13.0...v0.14.0 [0.13.0]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.12.0...v0.13.0 [0.12.0]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.11.0...v0.12.0 [0.11.0]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.10.1...v0.11.0 [0.10.1]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.10.0...v0.10.1 [0.10.0]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.9.0...v0.10.0 [0.9.0]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.8.0...v0.9.0 [0.8.0]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.7.1...v0.8.0 [0.7.2]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.7.1...01b1fe0 [0.7.1]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.7.0...v0.7.1 [0.7.0]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.6.1...v0.7.0 [0.6.1]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.6.0...v0.6.1 [0.6.0]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/OpenTodoApp/OpenTodo/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/OpenTodoApp/OpenTodo/releases/tag/v0.1.0