Changelog

Jump to a version
  1. 1.0.0
  2. 0.24.0
  3. 0.23.0
  4. 0.22.0
  5. 0.21.0
  6. 0.20.0
  7. 0.19.0
  8. 0.18.1
  9. 0.18.0
  10. 0.17.0
  11. 0.16.0
  12. 0.15.0
  13. 0.14.2
  14. 0.14.1
  15. 0.14.0
  16. 0.13.0
  17. 0.12.0
  18. 0.11.0
  19. 0.10.1
  20. 0.10.0
  21. 0.9.0
  22. 0.8.0
  23. 0.7.2
  24. 0.7.1
  25. 0.7.0
  26. 0.6.1
  27. 0.6.0
  28. 0.5.0
  29. 0.4.0
  30. 0.3.1-rc.1
  31. 0.3.0
  32. 0.2.0
  33. 0.1.0

1.0.0

· On GitHub

OpenTodo 1.0. From this release on, OpenTodo keeps the promises in Stability and compatibility: the API, the sync protocol, the export format and the upgrade path stay compatible within 1.x, and every release upgrades from every earlier one. 1.0 adds no feature. It is the release where the trust work of the last months comes together: a published threat model, how we verify it and the documented limits, with the hardening they called for (0.23.0 and 0.24.0); an accessibility statement against WCAG 2.2 AA (partial conformance: keyboard and 200% zoom are tested on every core screen, screen-reader passes are still to come); and performance budgets for a 10,000-task account that pass every night. Getting those budgets to pass made long lists much faster: completing a task in a 2,000-task project on a phone now takes about 100 ms instead of over 200, note search stays quick right after a first sync, and a sync no longer stalls while the database tidies its log.

All changes 12

Upgrade notes

  • Replace the image or the binary and restart. No migration and no new synced field since 0.24.0, and no new mandatory variable. Any release from 0.1.0 on upgrades directly to 1.0.0, with no version in between (upgrades); back up first, read the upgrade notes of the releases you skip for settings that changed, and upgrade the server before the devices.
  • The server checkpoints its database log in the background. SQLite's write-ahead log (opentodo.db-wal) may now grow to about 16 MB during bursts of writes before it is copied back, and is trimmed when it passes 64 MB; nothing to configure.
  • Release candidates: 1.0.0-rc.1 testers can upgrade in place.

Changed

  • Completing tasks in a long list is faster, above all on phones. A long list now lets the browser skip whole blocks of about 25 rows far from the screen, instead of deciding for each of 2,000 rows on every frame and every tap; screen readers still find every row in its one list. A completed row now leaves the screen by itself when its animation ends, so the list is drawn once per completion instead of twice. In the 2,000-task project of a 10,000-task account on a slowed phone, completing a task now responds in about 100 ms instead of about 220 ms.
  • Lists, counts and the app badge follow each change instead of starting over. After completing, editing or syncing a task, the list, its groups, the progress and blocked marks, the flow of a project in sequence, Today's closed list, the sidebar's counts and the app badge are now worked out from the tasks that changed rather than from every task, and give exactly what working everything out again gives.
  • Searching notes right after a first sync no longer stalls. The first keys typed into the note search on a phone could wait a quarter of a second while the app finished writing the sync's activity and built its search index; both now run in short pieces between keystrokes, and preparing a note for search is about twice as fast.
  • A sync no longer waits while the database tidies its log. Each time SQLite's write-ahead log reached about 4 MB, the write that filled it also copied the log back into the database before it could answer, and took two to three times as long. A background job now does that copy about a second after changes, beside the writes, and truncates the log once it passes 64 MB. In the 10,000-task account, the slowest pushes of 100 changes dropped from about 47 ms to about 24 ms on a development machine. Durability is unchanged.
  • Less work after completing a task in a long project. After each completion the app no longer counts every task of the project to decide whether to offer archiving it; it stops at the first open task. A tree list's sibling positions for screen readers (aria-posinset, aria-setsize) are now written in idle moments, a few rows at a time, instead of in the same block as the list's redraw. In a 2,000-task project on a slowed phone, an edit from another device now shows in about 0.7 s instead of 2 to 3 s while tasks are being completed.
  • Screen-reader passes no longer hold a release back. OpenTodo 1.0 claims partial accessibility conformance: the keyboard-only and 200% zoom passes are done on every core screen, and no NVDA, VoiceOver or TalkBack pass is recorded yet. Those passes are recorded when someone runs them; to contribute one, follow the steps in docs/accessibility/audit-1.0.0.md and post the results on #9. make release-prepare names how many screen-reader passes are pending in one warning line. It still refuses a pending keyboard or zoom pass, a failure without an issue link, and an audit more than 12 months old.
  • No real device was measured for 1.0, and the docs now say so. The performance budgets 1.0 promises are checked every night in CI only: in desktop Chromium, in a phone profile (the CPU slowed four times and a phone's screen, against a local server) and on the server. The project has none of the reference devices (a 2022 mid-range Android phone, an iPhone 12, a Raspberry Pi 4), so measuring them moves to after 1.0 (#990). make release-prepare no longer requires docs/performance/results-x.y.0.md for a minor release, and a release candidate no longer warns about it; a record that exists is still checked, and a value over its budget still needs an issue link.

Fixed

  • Android widget task rows follow light and dark mode. The Today widget drew its task rows in the colour of the moment it was last drawn, so after a theme switch they could stay light grey on a light background. They now use the same day/night colour as the rest of the widget, and overdue rows a slightly darker red that stays readable in light mode.
  • A Things 3 database can be imported from an iPhone. The file picker on Settings → Import now offers ZIP archives and SQLite files, so the zipped Things Database.thingsdatabase folder is no longer greyed out on iOS. The Things guide explains the steps on iPhone and iPad.

0.24.0

· On GitHub

OpenTodo 0.24.0 is a security and fixes release on the way to 1.0. ChatGPT and Claude (claude.ai) can connect with OAuth again; since 0.23.0 only Gemini could. The audit log is now tamper-evident, a device that answers AI connectors for an encrypted account shows what it reads and can ask first, and another site can no longer sign you out. In the installed iPhone app, no control sits under the Dynamic Island or the home indicator. Also new: an accessibility statement against WCAG 2.2 AA.

All changes 14

Upgrade notes

  • Replace the image or the binary and restart. One migration runs at start (0060: the audit log's hash chain); there are no down-migrations, so back up first. Events recorded before this release are not part of the chain. No new synced field and no new mandatory variable.
  • An OAuth client you created for ChatGPT or Claude under 0.23.0 has Gemini's redirect addresses. Delete it and create a new one for the right app, or let ChatGPT or Claude register themselves.
  • On a host, the assistant's limit of 8 answers at a time is now shared by all tenants.

Security

  • The audit log is tamper-evident. Each event now carries a hash of itself and of the event before it, so an event changed, removed or moved in the database outside OpenTodo shows up as a broken link; pruning by retention keeps the chain verifiable. The server checks the chain at startup and hourly while events are recorded and logs the first broken event; the owner can check it with ot audit verify or GET /api/v1/audit/verify. Every export ends with the chain head, to keep outside the server and compare with later exports. Events recorded before this release are not covered. See Checking the log for tampering. (OpenSpec audit-hash-chain)
  • On a host, the assistant's limit of 8 answers at a time is shared by all tenants. In host mode each tenant used to allow 8 running assistant answers of its own, so the operator's provider keys could see 8 for every open tenant. The 8 are now one limit for the whole host; beyond it a new answer gets the usual 503 assistant_busy with Retry-After, in any tenant. A self-hosted server is unchanged. See Host mode. (OpenSpec host-assistant-concurrency)
  • Another site can no longer sign you out. A sign-out request that the browser marks as coming from another site (a foreign Origin, or Sec-Fetch-Site other than same-origin) is now refused with 403 bad_origin instead of ending the session and clearing its cookie. SameSite=Lax already kept unrelated sites out, but a page on a sibling subdomain counts as the same site and could do it. The app's own pages and ot logout sign out as before. Found by the route matrix, which now also checks every public route anonymously, with each token scope and from another site against what the route promises. (OpenSpec external-security-review)
  • A device that answers AI connectors shows what it reads, and can ask first. For an end-to-end encrypted account, each request the server sends an opted-in device now names the connector, the tool and the call's arguments. While the device answers, the key in the sidebar says which ("Answering Claude.ai: list_tasks"), and Recent requests lists reads as well as changes with the connector's name. The new Confirm reads first (Settings → Integrations → AI assistants, off by default) shows each read with the text it would send before anything leaves the device, with Allow and Decline; a declined read gives the assistant declined_on_device. A request that names no tool is refused. The names come from your server, so the device cannot prove an assistant asked; see Cloud connectors through your device. (OpenSpec relay-request-transparency)

Added

  • An accessibility statement. Accessibility says how far OpenTodo meets WCAG 2.2 Level AA, criterion by criterion and screen by screen, how each part is tested, and how to report a barrier (answered within 10 business days). It claims partial conformance for now: keyboard-only use and 200% zoom with large text pass on every core screen, while the passes with NVDA, VoiceOver and TalkBack are still to be done on real devices (audit record). A release is refused when the audit is more than a year old, and 1.0.0 until those passes are recorded. Linked from Help.

Changed

  • The task completion budget is measured like INP. The performance benchmark times completing a task in a long list with the browser's own Event Timing, from the click to the paint that shows the tick, the way Core Web Vitals measure responsiveness; the earlier timer is still reported beside it (docs/performance.md).
  • Release candidates can be prepared and stay with testers. make release-prepare VERSION=x.y.z-rc.N writes only the candidate's CHANGELOG.md section and keeps the changelog fragments, so each candidate lists everything since the last release and 1.0.0 gets one complete section. The store templates, docs and Helm chart stay on the last release. A candidate tag no longer publishes the Claude or Obsidian plugin. The gates are those of 1.0.0, except that the reference-device record and the screen-reader passes, which need the owner's phones, only warn on a candidate (RELEASING.md, OpenSpec release-candidates).

Fixed

  • The docs pages show the current logo in the browser tab. Their favicon and touch icon links now carry a hash of the mark (/assets/favicon.svg?v=…), as the rest of the website does, so a new logo is never served from an old cached copy.
  • The browser extension's settings page: the Save button is readable. White on the lighter green fell below the contrast minimum; it now uses the popup's colours (extension 0.6.4).
  • ChatGPT and Claude (claude.ai) can connect to the MCP endpoint with OAuth. Since 0.23.0 only Google Gemini's redirect addresses were accepted, so ChatGPT and claude.ai stopped at redirect_uri is not registered for this client. In Settings → Integrations → AI assistants → Apps that sign in with OAuth, choose the app (Gemini, ChatGPT or Claude) when you create a client, or Other app with its exact https:// redirect address; ChatGPT and Claude can also register themselves. A client created earlier for ChatGPT or Claude has Gemini's addresses: delete it and create a new one. See OAuth clients.
  • No control sits under the Dynamic Island, the notch or the home indicator. In the installed app on an iPhone, sheets, dialogs, side panels, menus, toasts, display mode and the tab bar keep clear of the screen's safe areas in portrait and landscape, also with the keyboard open, so a Close button can always be tapped (#973).

0.23.0

· On GitHub

OpenTodo 0.23.0 is about trust. It publishes a threat model, a security retrospective and a disclosure policy, with documented limits, and ships the hardening they called for: OAuth refresh tokens rotate, AI agents get per-account limits and can no longer build or run routines that send data out, first-run setup under a domain asks for the setup code, cookie writes need Fetch Metadata, and imports, backups and single sign-on are stricter (see Security). The MCP endpoint now speaks OAuth, so apps such as ChatGPT, claude.ai and Gemini connect by URL. Also new: Simplified Chinese, a new logo, calendar events in Upcoming, a subtask's full path, comment webhooks, keyboard grouping and sorting, every drag with a one-click alternative, and the 1.0 docs (self-hosting handbook, configuration reference, contributor guide, roadmap). Many fixes come from your reports, including imported tasks no longer showing their import id.

All changes 49

Upgrade notes

  • Replace the image or the binary and restart; upgrade the server before the devices. Three migrations run at start (0057–0059: MCP OAuth clients and grants, API token expiry, refresh-token rotation); there are no down-migrations, so back up first. New synced fields user_prefs.today_assignees and user_prefs.upcoming_events, additive. No new mandatory variable.
  • First-run setup under a public domain name now asks for the setup code until OPENTODO_BASE_URL is set; installs opened by IP address, localhost or a LAN name are unchanged.
  • Backups and single sign-on no longer use HTTP_PROXY/HTTPS_PROXY from the environment; a server that reached them only through a proxy needs a direct route.

Added

  • Simplified Chinese. The app, reminder notifications, the browser extension, the Android and iPhone shells, emails and notification channels can use 简体中文. A browser set to zh-CN, zh-Hans or zh-SG follows it; zh-TW, zh-HK, zh-MO and Traditional Chinese stay in English. Dates use the browser's own Chinese locale. Quick add stays on its existing languages. (#883)
  • The logo files travel in the site bundle. The release's site bundle (opentodo-site-bundle-<version>.tar.gz) now has brand/kit/: the repository's brand kit, with the logo on light and dark backgrounds, in one colour and with the name beside it, the app icon from 16 to 1024 px, an avatar and two social images, as SVG and PNG with a README. The other brand/ files keep their names. The website's press kit offers these files.
  • A new logo. The OpenTodo mark is now an open tray with a tick dropping into it, in the app, the browser tab, the installed app's icon, the browser extension and the deploy catalogues. The iOS and Android apps get it too, and Android's launcher icon is now an adaptive icon with a themed version on Android 13 and later.
  • The docs website has clear entry points. open-todo.app/docs now opens on the search and six entry points (Get started, Use OpenTodo, Self-host, Integrate, Reference, Under the hood), each with its key pages and a page that lists all of its guides, the deeper ones nested under their topic. The sidebar follows the same six, the breadcrumb names them, and the header and footer are the website's. Pages read more easily (a narrower measure, calmer headings and code), take their colours from the app's own tokens, and no longer load fonts from Google.
  • Repeat on the first, second, third or fourth weekday of the month. Next to every last weekday, a rule can now be every first weekday, every 2nd weekday, every third weekday or every 4th weekday: that Monday-to-Friday day of each month, with weekends not counted. In March 2026, which starts on a Sunday, every first weekday is Monday 2 March and every 2nd weekday is Tuesday 3 March. It is stored as every 1st weekday, works with until and for N times but not after completion, and CalDAV apps see it as FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=1. Quick add understands it in every language (jeden ersten Werktag, chaque premier jour ouvré, ogni primo giorno feriale, cada primer día laborable, todo primeiro dia útil), and a Todoist import keeps every first workday. Quick add also reads ogni ultimo giorno feriale, cada último día laborable and todo último dia útil as the last weekday; it took them for the last day and left the last word in the title. (#834)
  • Group and order a list from the keyboard. In a project's list, on a label's page and in a saved view, r g and a letter sets Group by (s section, j project, n none, d due date, p priority, l label), r o and a letter sets Order by (m manual, p priority, d due date, c created, l deadline, t title), and r r reverses the order. So r g p groups by priority and r o d orders by due date. The keys change the same synced setting as Display options, only offer what the page's panel offers, and a short notice says what is set. Pause after r and the key card lists what can follow. They work with Vim keybindings on. See Keyboard shortcuts. (#842)
  • Today can hide tasks assigned to other people. Today's menu offers Hide tasks assigned to others once a shared project has someone else in it. It is off until you turn it on. While it is on, Today, its count, the icon badge, the briefing and the home-screen widget leave out a shared task assigned to someone else. Tasks with no assignee, tasks assigned to you, Inbox and projects this device does not know as shared stay. Upcoming, including when it starts with Today, is unchanged. Filters can say assigned, unassigned, assigned: me or assigned: "Name". (#827)
  • The phone's tab bar follows the sidebar order. Inbox, Today and Upcoming on a phone use the order from Settings → Appearance → Sidebar destinations, the same order as the sidebar. Search stays last, and Calendar and the other destinations stay in the drawer. Someone who never reordered still sees Inbox, Today, Upcoming, Search. The order is the one already saved with the account, so it is right offline and follows the other devices after sync. (#873)
  • Calendar events in Upcoming. Upcoming can show the appointments of each day above that day's tasks, the same read-only agenda as Today: all-day events as chips, then timed events with their time, title, calendar colour and location. Show calendar events in Upcoming's menu (next to Show repeating occurrences) is off until you turn it on, saved with the account as user_prefs.upcoming_events, and separate from Today's choice. Events stay out of the day's count, a filter does not hide them, a day that only has meetings still appears (through the last planned day, up to 92 days ahead), and hiding calendars on this device hides them here too. Offline, the list uses the cached events and says when they were refreshed. The first load grew 0.8 KB (Calendar events in Today, #808).
  • Apps that require OAuth can now connect to the MCP endpoint. Google Gemini's Connected apps refuses a pasted token ("Gemini requires standard OAuth for server connections"), so the server now answers as an OAuth 2.1 authorization server next to /mcp: discovery documents, dynamic registration limited to Google's connector addresses, authorization code with PKCE, a consent dialog where you allow the connection, access tokens that stop working after an hour, and a refresh token that keeps Gemini linked. The owner creates a client in Settings → Integrations → AI assistants → Apps that sign in with OAuth and pastes its id and secret into the app; the secret is shown once. Every access token is an ordinary API token — it appears as MCP: <client> in Settings → Integrations → API tokens — and a client can be cut off by deleting it here. See OAuth clients.
  • A subtask shows where it sits. The task page and the docked panel name the whole path, Work › Draft the budget › Gather numbers › Ask finance. Each part opens that task. When the path is long, the parents in the middle sit in a … menu that stays next to the header on a phone. Press u to open the parent. (#862)
  • Webhooks can subscribe to comments. A comment now fires comment.created, comment.updated or comment.deleted, with the comment and the task it is on in the payload, the same signature, project filter, retries and delivery log as other events. A comment an assistant wrote carries the device id agent:<token>, so a receiver can ignore its own writes. See Webhooks (#863).
  • Every screen is checked for accessibility on every change. Each pull request now opens every screen and overlay of the app at phone and computer widths, light and dark, and fails on any serious WCAG 2.2 AA problem axe-core finds, a focused control without a visible ring or hidden under a bar, a target smaller than 24 px with no space around it, sideways scrolling at 320 px (400% zoom), text cut off with wider spacing, or a password field that refuses a paste. Every night the same runs in English and German with large browser text. The 2,000-task list is checked as a screen reader gets it.
  • Single-key shortcuts can be turned off. If you use speech input or press keys by accident, turn off Settings → General → Keyboard → Single-key shortcuts on that device: a letter, number or symbol on its own (q, j, 1, ?, g then a key) then does nothing, while shortcuts with Ctrl, Alt or ⌘ and the arrow, Enter, Escape and Delete keys keep working.
  • A threat model, a security retrospective and a disclosure policy with response times. docs/security/ now publishes the threat model: the assets, the trust boundaries and thirteen attacker profiles, from an anonymous visitor to a malicious server for an encrypted account, a prompt-injected AI agent and another tenant in host mode. For each it says what is defended, by which mechanism, which tests would fail if that broke, and what is not defended. Security limits says in one place what is not protected: what the operator sees with and without end-to-end encryption, what is unencrypted at rest, what backups contain, and how far host mode and the demo isolate. How we verify security is the retrospective people asked for: the op-log sync and its convergence tests, the encryption format and the vectors shared by the app, the server and ot, the route policy test, token scopes, the SSRF policy, importer limits, upgrade tests, static analysis and fuzzing, every past security fix and the gaps still open. SECURITY.md now promises an acknowledgement within 3 business days and an assessment within 10, fix targets per severity (7 days for Critical, 30 for High, 90 for Medium, the next minor for Low), coordinated disclosure within 90 days with an advisory and a CVE, a safe harbour for research on your own instance, and which versions get fixes after 1.0; a repository check keeps those numbers from disappearing. Every server now answers /.well-known/security.txt (RFC 9116) with the same reporting channels and a link to the policy; a comment in it says that problems with how a particular server is run belong to its operator. The scope for an independent review is ready: no external review has taken place yet, and none is claimed. From 1.0, make release-prepare refuses a release whose review findings break the fix rule, once a review exists. (OpenSpec external-security-review, #588)
  • Docs for 1.0: a self-hosting handbook, a configuration reference, a contributor guide and a public roadmap. The self-hosting handbook takes an operator from an empty host to a monitored server in eight steps: install (every platform in one table), first run, reverse proxy and TLS (Caddy, nginx, Traefik), backups with a 10-minute restore drill, upgrading, monitoring, a hardening checklist and sizing from the measured budgets. The configuration reference lists every OPENTODO_* variable with its default and an example; a test fails when the code reads one that is not listed. Every error code the server sends is in the API table, checked both ways, and its Checked examples run against a test server on every change. The contributor guide has seven how-tos, from a first contribution in 30 minutes to adding an importer, a language or a shared contract, and CONTRIBUTING.md says how pull requests are made and how AI agents are used on the project. The roadmap page, also in Help under About, shows each change as shipped, in progress or planned, worked out from the repository at every build. The docs check now fails for a screen of the app that no guide explains, and an audit fixed settings paths, labels and language counts that had gone stale. (OpenSpec docs-1-0, #592)
  • Everything you can drag, you can also do with one click or tap. A task's menu has Reorder (Move up, Move down, Move to top, Move to bottom) in projects, the Inbox, a label's page and saved views; frames, a task's turn order and objectives have a handle that offers Move up and Move down when clicked; a click on the sidebar's or the task panel's edge switches between the widest and the usual width. A test now fails when a new drag has no such alternative (WCAG 2.2 2.5.7).

Changed

  • The new logo reaches the last places. The Home Assistant add-on's icon and logo (now 250 × 100, as Home Assistant asks), Runtipi's logo, the Claude plugin's icon, every screenshot in the docs, the README and the website, and the card shown when a docs page is shared now show the tray mark. The brand kit gains Google Play's feature graphic, and a check fails on any icon or logo that is not generated from the mark.
  • by in quick add sets the deadline. Install winter tires by March 31 now gives Install winter tires with a deadline of 31 March and no scheduled date, instead of scheduling it for that day. on and due still set the date you plan to do it, and the two combine: Pay tax tomorrow by friday is planned for tomorrow with a deadline on Friday. A time keeps by a due time, since a deadline has no time: Call back by 5pm is today at 17:00 (the word no longer stays in the title), and Pay tax by friday 5pm is due Friday at 17:00. German bis, Italian entro and Portuguese até work the same way; French pour and Spanish para still set the due date. The app, ot add --natural and the Obsidian plugin read lines the same way. See Deadlines. (#890)

Fixed

  • Imported tasks no longer show an internal id, and list columns line up. After a Todoist import every row ended with a code such as dfc92cb10aed05a4:1, the key the importer keeps to spot duplicates, and rows with something at their end had their date and labels further left than the others. The id is now shown nowhere: the task's details, panel and history and the command palette read "Imported from Todoist" (or the app it came from), ot list shows "Todoist", and a Trello card's link is named "Imported from Trello". On a list row wide enough for the property column, the row's end is one width whether the task has an origin or not, so dates and labels sit in one column down the list; a long origin there ends with an ellipsis (OpenSpec import-origin-reads-as-app). (#945)
  • A layout chosen while a project is still opening is kept. Until the project's own layout (Board, Timeline, Calendar) had loaded, the switch showed List as chosen and a click on List was ignored, so the project then opened in its previous layout anyway.
  • The app's first load is about 8 KB smaller. The QR code of the two-factor enrolment wizard and its encoder now load when the wizard opens instead of with every first visit; a square of the same size holds its place until it is drawn.
  • Ticking a task in a long list is quicker. A click on a task's circle drew the whole list again in that frame; in a 2,000-task project it now draws only that row, and the list catches up when the task leaves it. The completion stays the same to look at and to a screen reader. (#854)
  • A saved view in display mode lists the same tasks as the view. Display mode ran a saved view without your workspaces, the people you share with, custom fields, project flows and tracked time, so a view whose query uses workspace:, assigned:, a custom field, available or tracked showed no tasks there. It now uses the same information as the view's own screen. The sidebar's count of a view on a custom field counted nothing for the same reason and now matches the view. (#901)
  • Settings fixes from the settings audit. The audit log and Recent activity name ten events they showed as raw codes (calendar connections, task pages, the project AI-assistant switch, and OAuth clients and connections for AI apps), and they can be filtered; a test now keeps the web list in step with the server's catalogue. On Security, Sign out everywhere is the button styled as dangerous, not the sign-out of this device. Turning on Require single sign-on asks for confirmation first. The OAuth client name field clears after a client is created, and a long backup folder path no longer runs off a phone screen.
  • The title can be corrected while processing the Inbox. Clicking it showed a cursor, but the decision keys took every letter, so a voice-to-text line could not be fixed without leaving the mode. Click the title or press Tab, type, and Tab, Enter or a click away saves it; Esc puts the old title back. The decision keys work again once the field is left. Enter still opens the task beside the card for comments, subtasks and the rest. (#858)
  • Voice input in the Android app can ask for the microphone. The app never declared the microphone, so Android showed no prompt and no switch in the app's settings, and the page told you to allow it from an address bar the app does not have. The first time you dictate, Android asks; allow it and the page can record. A refusal says to allow the microphone for OpenTodo in the phone's settings. (#866)
  • A board from a template stays in the workspace you are in. Choosing New board → Eisenhower matrix in a workspace other than the default saved the board and its four filters in the default workspace, so they were missing from the sidebar you were looking at. They are now saved in the current workspace, the same as an empty board. (#870)
  • A long title in the task panel shows in full. The title stopped at two lines, so a longer one had to be edited inside a box that hid most of it. The title on the task page and in the docked panel now grows with the text, up to eight lines, and scrolls only past that. (#859)
  • A location can be a name that is not on the map. Typing "Garage" or "Mum's" could not be saved unless the map search found it, so a place with no address never stuck (#817). The location field now offers Use “Garage” first; Enter keeps that name, with no address and no pin. An address the search finds stays in the list under it. Saved locations are unchanged. See Locations.
  • Keyboard and screen reader fixes across the app. While a dialog or a phone sheet is open, the page behind it can no longer be reached with Tab or a screen reader. A focused menu item shows the focus ring. A reminder, a break reminder or the daily check no longer covers the row you tabbed to (on a phone it moves to the top), the sidebar no longer hides a toast on the left, and a header or the filter's Save bar no longer covers the control you tabbed to. Inside dialogs and sheets too (the help panel, the task page), the focused control is scrolled out from under their sticky headers. A row's actions no longer cover the cut project name you tabbed to, and with large text the sidebar's account button and sync status fit, the Help page's tasks and a date picker's month no longer push the page sideways. Each section of a project list is announced with its name. The weekly review's steps fit a 320 px screen (they wrap at 400% zoom with large text), and the task page's breadcrumbs, the feed's project links, the board's column dots and the time zone's Change are easier to hit.
  • Password managers fill the account form of a workspace invitation. Its email, name and password fields now say what they hold, like every other sign-in form.
  • Readable text on the iPhone app's first screen. The "Server address" heading, the line under it and the error messages were light grey, a little under the contrast small text needs; they now use the full text colour.
  • A task's right-click menu opens complete and stays where it appears. The menu was shown before it had read the task it was opened for, so its first frame lacked the task's own items (Add to focus, Start tracking, Start focus), which then appeared and pushed the menu to another place. It now opens once those local reads have landed, a few milliseconds later, with every item and in its final place; the same holds for the row's More button, the keyboard's menu and the Selection bar's Actions (OpenSpec context-menu-first-paint). (#840)
  • A bulk action that empties the list closes its menu and opens nothing. On a label page with every task ticked, removing the label through Actions → Labels… turned the still-open menu into one task's context menu at the far left of the window, and the last ticked task opened in the side panel. The Actions menu now closes once the ticked tasks have left the list, and the side panel follows the cursor only while its task is still listed (OpenSpec bulk-menu-after-empty-list). (#841)
  • Task titles are readable on a phone after an import. Tasks imported from Todoist showed the importer's internal id (such as dfc92cb10aed05a4:1) at the end of every row, and in the Inbox the line "Imported from todoist" stood beside the title instead of under it, so on a phone the title was cut to a character or two. A row no longer shows an imported task's id: an import without a link shows nothing there, a Trello card shows its icon as the link. On a row narrower than 30 rem any origin is its icon, on a phone it moves to the row's details and the task page, and the Inbox's source line is the line under the title (OpenSpec row-title-first-on-phones). (#844)

Security

  • run_routine now stays within an AI assistant's daily write limit. A routine run started over MCP counted its writes toward the token's 1,000 items a day only after it ran, and it ran even when nothing was left. It now runs only within what is left: with none left it answers rate_limited, and a run that would write more than is left writes nothing and is logged as failed with the reason. Tests are not limited. See Limits.
  • An AI assistant can no longer create, change, turn on or run a routine that sends a webhook or an email. Routines with Send a webhook, Email me or Email project members steps are now the person's alone. Over MCP and in the in-app assistant, create_routine, update_routine, set_routine_enabled (turning on) and run_routine answer forbidden_for_agents for them. An assistant can still read, test, turn off and trash them, and nothing changes in the app. This makes the MCP guide's promise that no tool sends anything off the server hold for routines too.
  • AI agent write limits also apply per account. All of one person's agent tokens and connected OAuth apps together may now make at most 60 write calls a minute and change at most 2,000 items a UTC day, next to the existing 30 calls and 1,000 items for each token, so creating more tokens no longer raises the budget that contains a runaway or misled agent. Over a limit the call is refused with rate_limited and nothing changes. See MCP (OpenSpec agent-limits-per-account).
  • Backups and single sign-on no longer use a proxy from the environment or follow a redirect to another host. The backup client took HTTP_PROXY/HTTPS_PROXY from the server's environment and followed redirects to any host; the single sign-on client also took the proxy. Both now connect directly to the destination or issuer the owner configured, as webhooks and every other outbound request already did. A backup destination may still redirect up to three times within its own scheme and host (a WebDAV folder without its trailing slash, for example); a redirect elsewhere, or from https to http, fails the run, and the sign-on client still follows none. Destinations and issuers on your own network keep working. If your server reached a backup destination or your identity provider only through a proxy, it now needs a direct route. See Backups (OpenSpec operator-clients-no-env-proxy).
  • Imports refuse oversized CSV fields and deeply nested JSON. A file within the upload size and the 50,000-row limit could still hold one enormous field, a record with a very large number of fields, or deeply nested arrays, costing far more memory than any real export. Every CSV import (CSV, Todoist, TickTick, Linear, Asana, OmniFocus, Notion) now refuses a field over 1 MiB, a record over 4 MiB or with more than 1,000 fields, and a JSON file, also inside a ZIP, nested more than 256 levels deep. The import answers too large with the limit and, for CSV, the line, and nothing is imported. Exports from supported apps stay far below these limits. See Import (OpenSpec importer-field-limits).
  • First-run setup under a domain name asks for the setup code until a base URL is set. While a server has no owner and no OPENTODO_BASE_URL, it cannot tell its own name from another site's name pointed at it, so a first registration addressed to a domain name now needs the one-time setup code from the server log, even with OPENTODO_SETUP_CODE off. Opening the server by its IP address, localhost, a name without a dot or a local name (.local, .lan, .home.arpa, .internal and a few others) needs no code, so a fresh Docker install works as before; with OPENTODO_BASE_URL set nothing changes. The setup screen says why it asks and offers the IP address or the base URL instead. See The setup code (OpenSpec host-check-without-base-url).
  • Cookie writes without an Origin need Fetch Metadata or a JSON body. A change sent with the session cookie but no Origin header used to be accepted, relying on the cookie's SameSite=Lax alone. It is now refused with 403 bad_origin when the browser's Sec-Fetch-Site says it came from anywhere but the app's own pages, and, when it names neither header, unless it sends a JSON body: a form, multipart or bodiless request, which is all another site can make an old browser send, is refused. Every supported browser, the Android and iOS apps, the service worker and ot login keep working; API tokens are unaffected (OpenSpec cookie-writes-need-origin).
  • OAuth apps on the MCP endpoint get a new refresh token on every refresh. The refresh token an app sent stops working once it has been exchanged, and if an old one is sent again, the server ends that connection: its refresh and access tokens stop working, the app has to be connected again, and the audit log records mcp_oauth.refresh_reused. Clients registered only for Google's connector addresses, which Gemini uses, keep one refresh token, because Google's guide requires it. See OAuth clients (OpenSpec mcp-oauth-refresh-rotation).
  • Encrypted accounts refuse a key record weaker than the one the app writes. The app and ot accepted a key record with 100,000 PBKDF2 iterations or more, while the app writes 600,000 and the server stores only 600,000 to 10,000,000. They now accept only the server's range, so a server cannot get a device to unlock with a cheaper derivation than the app uses. Every key record a server holds already uses at least 600,000, so no device is locked out and nothing has to be re-entered (OpenSpec e2ee-iteration-floor).
  • The share target keeps a share only when the browser says where it came from. The installed app stored a share that carried no Sec-Fetch-Site header, which older browsers leave out, so it could not tell the system share sheet from a form on another site. Such a request is no longer stored; the capture screen opens empty and says to update the browser. Every browser that can share to an installed web app sends the header, and the Android and iOS apps hand shares over another way, so shares keep working (OpenSpec share-target-fetch-metadata).

0.22.0

· On GitHub

OpenTodo 0.22.0 is about speed at scale and switching in. Big accounts are much faster: in a 2,000-task project completing a task went from about 4 seconds to a tenth of one, a new device's first sync of a 10,000-task account is about ten times faster, and a year of note history is compacted in minutes while a new device downloads only the latest notes. Lists, label pages and saved views can now sort by custom fields, and on Android Back closes what is open. You can now switch from OmniFocus, Vikunja, Notion, Super Productivity and Tududi, chosen from a new Switching from… list on the Import screen. And there is a written stability promise for 1.0, checked on every change: the v1 API and data formats only grow, and upgrades from every earlier release are tested.

All changes 42

Upgrade notes

  • Replace the image or the binary and restart; upgrade the server before the devices. One migration runs at start (0055, an index for listing a project's tasks); there are no down-migrations, so back up first. No new variable and no new synced field.
  • Sync gains an optional latest_docs feature that upgraded apps use for a first sync; older apps keep getting the full history and keep working.
  • The first notes compaction after upgrading may run for a minute or two on a large, old database, in short steps that never hold writes for more than about 20 ms.

Added

  • A written stability promise for 1.0, checked on every change. Stability and compatibility says what every 1.x release keeps: the /api/v1/ routes with their fields, statuses and error codes, the sync protocol and op schema, the export format, the MCP tools and the ot commands, flags, exit codes and --json output. Within 1.x these only grow; a route that will be replaced is deprecated for at least 12 months and two minor releases first and answers with Deprecation and Sunset headers. Any release from 0.1.0 on upgrades directly to the latest, and every 1.x server works with every 1.x client and imports every export an earlier version wrote. Tests enforce it: a snapshot of the whole surface that may only grow, an upgrade of a data directory written by every released version from 0.1 to 0.21, the recorded calls of the web app, ot and the extension replayed against the current server, and every released version's export imported. From 1.0 on the image also gets a 1 tag that follows the 1.x line (#587).
  • Switching from… on the Import screen, and a migration matrix. Choose the app you use now at the top of Settings → Data → Import data: its export steps appear right there, the format is set for you, and Full guide opens its switch guide. Switch from another app lists all fifteen apps (Todoist, TickTick, Things, OmniFocus, Any.do, Microsoft To Do, Google Tasks, Apple Reminders, Vikunja, Trello, Asana, Notion, Super Productivity, Tududi and Linear) with what comes with you, what stays behind and when each import was last checked against a real export; a repository check keeps every row backed by an importer, a guide and a fixture, and release preparation for 1.0 refuses a check older than 120 days. ot import --help lists every format.
  • Import from Vikunja. Upload the ZIP from Vikunja's Settings → Export your data and your projects, sub-projects, Kanban buckets (as sections), tasks, labels, repeats, reminders, subtasks, "blocked by" relations, comments and attached files come with you. Verified against exports of Vikunja 0.22.1 and 2.7.1; see Switch from Vikunja.
  • Import from Tududi. Upload a copy of Tududi's database file (production.sqlite3) and your areas, projects with their notes, tasks, subtasks, tags, priorities, dates and repeats come with you, read from a private read-only copy that is deleted afterwards. Verified against Tududi 1.14; see Switch from Tududi.
  • Import from Super Productivity, tracked time included. Upload the backup from Super Productivity's Settings → Sync & Backup → Export data and your projects, tags, tasks, subtasks, archived tasks, due dates, deadlines, estimates and repeats come with you. The time you tracked arrives as one time entry per day, so your totals stay the same. Verified against Super Productivity 19.3.1; see Switch from Super Productivity.
  • Import from OmniFocus. Upload OmniFocus's CSV export (File → Export → CSV, or CSV UTF-16) and your projects, actions and action groups as subtasks, tags, due, defer and planned dates, estimates, flags and notes come with you. A newer export of the same database skips what you already imported. Repeat rules are not in OmniFocus's CSV, and the preview says so; see Switch from OmniFocus.
  • Import from Notion, with your own columns. Upload the ZIP from Notion's Export → Markdown & CSV: each task database becomes a project, each row a task with its page as the description, and sub-items stay under their parent. Because Notion's columns are your own, the preview proposes which column holds the status, due date, labels, priority, project and notes, and you can change each one (or leave it out) before importing. The API takes the same choice as the columns option; see Switch from Notion.
  • The import preview names the format a file really looks like. When you choose a format and the file is another app's export, the preview says which one (detected in the API).
  • GET /api/v1/tasks?project_id=<id> lists one project's tasks. The parameter used to be ignored, so the call listed every task of the account. It now lists only that project's items, on every collection whose items have a project, and reads only that project's rows: about 150 ms for a 2,000-task project in a 10,000-task account. An empty value or a collection without projects answers 400 invalid_input (API).
  • Performance budgets for a 10,000-task account. Performance budgets lists how fast OpenTodo is held to be on a computer, in a phone profile and on a small server, with a test account built by go run ./cmd/perfseed and a benchmark you can run with make perf (#590).
  • Sort lists by custom fields. Project lists, label pages and saved views can sort by Number or Date fields. Same-named fields match across projects, including inherited fields; a sort follows the field's stable definition ID after a rename and falls back to the page default when that definition is deleted. Sync carries the ID rather than the encrypted field name. (Group and order, Custom fields)
  • Suggest after adding a task. A new per-device switch, Settings → Integrations → Assistant → Suggest after adding (off by default), makes the assistant propose a clearer title, a project and up to four subtasks right after you add a task from a quick-add line. The proposal is the usual reviewable change set: nothing is written before Apply (even with Apply small changes without review on), and a task added while the assistant is busy gets no suggestion. Undo puts it back. (Suggest after adding, assistant-suggest-after-add)

Changed

  • Getting a chore's turn because someone left now tells you. When the person whose turn it is leaves a shared project, is removed or deletes their account, the next person in the rotation now gets the same It's your turn notification as after a completion, in the app, by push and on their notification channels. Before, the task moved to them silently (Take turns).
  • A new device's first sync of a large account is about ten times faster. Signing in to a 10,000-task account took over two minutes before everything was on the device; it now takes about 12 seconds on a laptop. The device keeps the newest 100 changes of each project in its local history after such a sync; the Activity tab and Updates load older history from the server, as before.
  • Long lists stay quick. In a 2,000-task project, completing a task used to take about 4 seconds on a computer and 15 on a phone before the tick showed; it now shows in the same frame as the click, and opening the project takes about 0.1 second instead of 2. Moving with j and k, an edit arriving from another device and typing in the command bar no longer redraw every row. Rows below the screen are drawn a moment after the top ones and stay in the page, so find in page, drag and drop and screen readers reach all of them as before. Ticking several tasks in a row on a phone no longer makes each tick wait for the list to work out all its rows again: a write redraws only the rows it changed, and the counts of saved views, workspaces and the app icon badge follow a moment after the ticks stop. The command bar prepares its search while the app is idle, so its first keystroke is as quick as the rest.
  • A year of note history is compacted in minutes, and a new device downloads only the latest notes. With 500 notes saved 300 times each over a year (2.6 GB), the compaction of old note versions never finished a pass and could hold a sync back for seconds; it now finishes in a few minutes of background work, works in steps of about 20 ms so a change saved meanwhile waits a moment at most, and returns the space as it goes (2.6 GB to 240 MB). A device signing in for the first time asks only for each device's latest copy of a note instead of every saved version: on that account it downloads 43 MB instead of the 2 GB a new device pulled before. Older apps keep working and download the whole history as before (Sync protocol, #590).
  • The assistant gets 12 steps for a multi-step request. A turn that keeps planning ("then break each of these down") reached round_limit after 8 model calls, so the loop now allows 12 on the server and on a device, and the fixed instructions ask the model to read what it needs in one step and send independent tool calls together. A tool-use answer on the 12th call still stops the turn with round_limit and no change set; an end_turn on the 12th call keeps its change set. (API, assistant-suggest-after-add)

Fixed

  • The task's Comments, Activity, History and Time tabs on a phone in a long translation. In German and French the row was wider than a 360 px screen and its last tab sat cut at the edge. The row still scrolls sideways inside itself, now fades out at the edge where more tabs are hidden, and keeps the selected tab clear of the fade; this holds for every tab row. Safari also draws every list row again: its engine sometimes left the rows on screen undrawn after the faster list rendering of this release, so a list could open blank.
  • The floating add button no longer covers fields on Import and other screens without quick add. The 0.21.0 fix left it out of the Settings overview only; on a phone it still sat over the import options. Each screen now says whether it offers quick add, and the button stays off Settings and its sub-pages, the theme editor, Import, Help, Updates, Statistics, Trash, Routines and the sign-in and share links. The layout check fails when the button covers a form field.
  • ot vault sync and watch no longer overwrite a file saved while a sync runs. Each sync reads a project file or task note again right before writing or removing it; a file saved meanwhile is left as saved, and the next sync merges it with what changed in the app in between. New lines in it still get their 🆔, so no task is created twice.
  • Labels that were cut with room to spare. A repeating task's This occurrence / All occurrences switch read "This occurren…" even on a wide screen: its options split the row evenly, so the longer label lost. The options of every two- or three-way switch are now as wide as the longest label and only shorten when the row itself is out of room. In the Week calendar beside the planning list, a narrow day header put the load next to the day's name and cut the name to "T…"; a narrow header now puts the load under the name. The layout check fails when a switch option or a calendar day name is cut.
  • The first screen no longer downloads Settings, the Calendar or Import. Those screens, and a project's Calendar layout, now load when first opened, with the app's row skeleton meanwhile; the first load fell from 745 KB to 648 KB gzip (the entry chunk from 312 KB to 135 KB), and every screen still opens offline because its chunk is precached. (OpenSpec)
  • A server with calendar feeds and calendar sync off no longer logs a 404 on every app start. The app now reads the two owner switches before asking for events, so the default install makes no request to the disabled events route and drops any events cached while the feature was on; once either switch is on, the next refresh fetches them. (OpenSpec)
  • A long panel title shows its start again after editing. A two-line title scrolled inside its field while it was edited kept that position after the edit, so the beginning stayed hidden. Leaving the field now scrolls the title back to its start.
  • The installed app's splash is the dark canvas. The manifest's splash background was a slate #0f172a that matches no part of the app; it is now the dark canvas #0d0e10. A manifest has one splash colour, so a light device sees a dark splash before the light page, which is the lesser flash compared with a bright splash on a phone in dark mode.
  • The installed app's status bar keeps its colour from the first frame. The static theme-colour metas, the boot script's light fallback and the manifest still said #0f172a, while the default emerald appearance uses #047857 in light and #0d0e10 in dark, so Android changed the tint after the app loaded. They now use the default bars, and a test keeps the three files in step.
  • The floating add button no longer covers Settings rows on a phone. Settings has no quick-add action, and the button sat over the chevrons at the end of its rows. On a phone the tab bar stays, and the button returns on the next view that offers quick add.
  • Undo a big delete. A bulk Trash offer held Undo for only ten seconds, and Delete on a large ticked set trashed it without asking. The offer now holds Undo for fifteen seconds and offers Show Trash, and Delete on more than twenty ticked tasks asks first with the count; smaller sets stay immediate (OpenSpec trash-undo-toasts).
  • Deleting a task always offers Undo. The command palette, Vim's dd, the Filter view and saved views wrote a bare tombstone and skipped the Undo toast the main list shows. They now use the same undoable trash, so every delete restores the task and its subtasks the same way (OpenSpec trash-undo-toasts).
  • A failed Focus time entry is no longer silent. When a session could not start or stop its time entry, the minutes were lost with only a console warning. The session now keeps running and a toast says whether it is not being tracked or its minutes may not be logged, with the underlying error (OpenSpec trash-undo-toasts).
  • A failure toast never covers the Undo offer. A focus time-entry failure could render a second toast region over the offer's Undo control. It now waits while an offer shows, takes the region when the offer leaves, starts its five-second window then, and pauses on hover or focus like the offer itself (OpenSpec trash-undo-toasts).
  • A failure toast keeps clear of the focused field. The focus failure message could sit over the field the person was typing in, and when its close button had focus, removing it left the next message held open. It now moves to the opposite corner like the Undo offer, and its hover or focus hold resets when the message changes or leaves (OpenSpec trash-undo-toasts).
  • Escape closes a failure toast and returns focus. The focus failure message had no keyboard dismissal: Escape with focus on its close button left the toast open with its timer paused. It now closes on Escape and returns focus to the element that had it before (OpenSpec trash-undo-toasts).
  • Move to project from a task's right-click menu moves the task again. Picking a project closed the menu and nothing else happened: the review of the move belonged to the menu, which the right-click menu, the row's More button and . remove as they close, so the review never appeared and the task stayed where it was ("Pick a date…" lost its date dialog the same way). The review, and the date, now appear however the menu was opened, and Apply moves the task as from the selection bar (#839).
  • Clearer feedback for a missing project, an empty Projects view and a sync error. Known local refusals such as a project deleted on another device now read in the interface language instead of English. The empty Projects view shows the shared empty state with one line distinguishing projects from labels and a New project action. A sync error keeps the status label and shows its message beside it (over two lines instead of cut short), and in the narrow icon sidebar tapping the status shows the full message in a toast, instead of hiding it in a tooltip.
  • Back on Android closes what is open, instead of the page behind it. The back gesture and button dismissed nothing: a sheet, a menu, the command palette or the phone drawer stayed over whatever page the browser had moved to. On a touch device (not in Safari or other iOS browsers, which have no such Back) every overlay now owns one history entry at the address it was opened from, so Back closes the topmost overlay and leaves the page where it is; a second Back navigates as it always did, and closing an overlay with its own control or Escape removes its entry so a later Back never reopens it. A destination chosen from the drawer takes the drawer's entry, so Back returns to the page the drawer was opened from (OpenSpec phone-interaction).
  • The closed phone drawer no longer takes the tab order or the screen reader. Below 768 px the drawer sat off screen but stayed in the document, so Tab walked through an invisible find field, destinations and account footer between the menu button and the page, and a screen reader read them on every screen. Closed, it is now inert and hidden from assistive technology; open, it is an overlay: focus moves into it, Tab stays inside it, the page behind it is inert and does not scroll, Escape or Back closes it, and focus returns to the menu button. From 768 px the sidebar stays the column it is, never inert and never hidden. phone-interaction in OpenSpec.
  • A tap on a row in Select mode ticks it instead of opening the task. "Select tasks" on a phone showed a checkbox on every row, but a tap on the title, or on the space beside it, still opened the task while the person was building a selection. With Select mode on, a tap anywhere on the row now ticks or unticks it, exactly as Shift and Ctrl/⌘ clicks do with a pointer; the checkbox, the details control, drag and swipe keep their own behaviour, and with Select mode off a tap opens the task as before (OpenSpec phone-interaction).
  • The quick add no longer covers the list on a phone held in landscape. From 768 px the quick add row is pinned under the page header, and a phone on its side is wider than that, so the pinned row and the header took most of the short viewport. The row now stays unpinned on a coarse pointer under 500 px tall and scrolls with the list, as it does on a narrow phone, leaving the floating Add button as the way to add; a keyboard cursor scrolled to a row no longer counts it as pinned there either (OpenSpec phone-interaction).

0.21.0

· On GitHub

OpenTodo 0.21.0 is about planning horizons and the home. Plan a task for this week, next week or this month without a made-up date (time sectors), and pin a few objectives for the quarter and the week above Plan and in the weekly review. A project can be a reusable list for groceries or packing, where adding an item that is already there brings it back instead of duplicating it. A repeating chore in a shared project can take turns between people. On Android, location reminders fire when you arrive at or leave a place (GitHub and F-Droid builds; Google Play follows). Also: buttons that only close a panel now say Close, the phone Day calendar leaves room for the hours, and a fix for encrypted accounts (see Security).

All changes 19

Security

  • Turning on end-to-end encryption now encrypts all your existing content, and finishes the job for accounts that already turned it on. When encryption is turned on, the app re-writes the text you already have as ciphertext. That job missed several kinds of content, so their text written before encryption was turned on stayed in plaintext on the server: notes (title, text and appended lines, since 0.19.0), time entry notes and the line for tomorrow from closing the day (since 0.9.0), saved locations (name, address and coordinates) and routine names (since 0.10.0), and calendar frame and slot titles (since 0.18.0). Text written after encryption was on was always encrypted. The job now covers every kind of content, and a test fails if a new kind is ever left out. If your account already encrypts, there is nothing to do: after the update, each unlocked device checks once, at the end of its next complete sync, and re-writes as ciphertext whatever of that text the server still holds in plaintext. Values already encrypted are left alone, so the check writes nothing when another device got there first. Run Purge plaintext history in Settings → Account → End-to-end encryption afterwards to delete the superseded plaintext operations; backups made before then may still contain that text. Turning encryption off now writes all of these kinds back as plaintext too.

Upgrade notes

  • Replace the image or the binary and restart; upgrade the server before the devices. No migration and no new variable. New synced entity objective and new synced fields project.reusable, project.aisles, section.aisle, task.sector, task.rotation, reminder.radius_m (with the reminder kinds arrive and leave), user_prefs.time_sectors and user_prefs.objectives, all additive: older devices keep working and ignore them, but only an upgraded server stores and serves them. Back up first; there are no down-migrations.
  • Encrypted accounts: after updating, open the app once on an unlocked device; it finishes encrypting older content by itself (see Security).

Added

  • Objectives for the quarter and the week. A few short lines that say what the quarter and the week are for, pinned where you plan (#580). Turn on Objectives in Settings → Account → How you work: the Plan shows this quarter's and this week's objectives above its columns, and the weekly review's Look ahead step lets you write next week's and carry, finish or drop last week's open ones (and last quarter's, in the first review of a new quarter). At most five open per period, one line each, edited in place; titles are end-to-end encrypted. Also in GET/POST /api/v1/objectives, ot objectives and the MCP tool list_objectives. See Objectives.
  • Take turns on a household chore. A repeating task in a shared project can now pass from person to person each time it is completed: the bins on Monday, the bathroom on Saturday (#582). Open the task's assignee and choose Take turns…, then pick the people, or Add everyone in a workspace group, and drag them into order (or Alt+↑/↓). Completing the task assigns the next occurrence to the next person, who gets an It's your turn notification; skipping or reassigning doesn't pass the turn. The panel shows Takes turns: Alice → Bob → Carol and the row marks the assignee with a rotate sign. When someone leaves the project, their turn goes to the next person. Every device that completes offline picks the same next person, and the REST API (rotation, 400 invalid_rotation, 409 not_a_member), CalDAV completions, ot edit --rotate/--no-rotate and the MCP update_tasks tool all work the same way (Take turns).
  • Time sectors: plan for this week, next week or this month without a fake date. A task can now be planned for a week, a month or a quarter instead of a day (#579). Turn on Time sectors in Settings → Account → How you work, then choose This week, Next week, This month, Next month, This quarter or Next quarter (or Later…) in the Sector tab of a task's When editor, run Set time sector… from the command palette on a selection, or type this week or sometime next week in quick add (in all six languages). A sectored task leaves the Inbox and Anytime, is never overdue, and keeps its deadline. Rows show the sector where the date would be, named from your date and first day of the week, and a sector whose period has ended reads From week 44 without anything being rewritten. In the Calendar, Plan from a list › Time sectors groups the tasks you can work on by sector, ready to drag onto a day, and the weekly review's Look ahead lists what was carried over and next week's tasks. Filters gain sector: this week, carried and no sector, and the API, ot --sector, the assistant tools, CalDAV (X-OPENTODO-SECTOR) and the Markdown vault ([sector:: 2026-W45]) carry the stored period. See Time sectors.
  • Reusable lists. A project can now be a reusable list for groceries, the pharmacy or packing (#581): Project menu → List… → Reusable list. Adding an item that is already there, such as "milk" when Milk was ticked off, reopens it in its section with its notes instead of adding a second Milk, and says "Milk is back on the list" with Undo and Add anyway; an item that is still open flashes instead. Names match ignoring case, accents and extra spaces, on your device, so it works offline. Ticked items stay at the end in a Got it group on every member's devices, and when two phones add the same item offline, the list settles on one (the other goes to the Trash, shown as a merged duplicate). Sort into aisles offers sections such as Produce and Dairy and eggs in your language and puts new items in the right one from a word list built into the app. POST /api/v1/tasks takes "reuse": true, ot add and the MCP create_tasks tool reuse items too, and ot project edit --reusable --aisles turns the mode on (Reusable lists, Sort a list into aisles).
  • Location reminders: when you arrive at a place or leave it. A task with a location can now remind you on your phone when you get there or go (#583): Add reminder → At a place → When I arrive or When I leave, with a radius from 100 m to 5 km (150 m by default). They fire in the Android app, while it is closed too, once you turn on Settings → Notifications → Location reminders → On this phone; the app first explains the use, then asks for location all the time. The phone watches the places with Android's own proximity alerts (no Google Play services), at most the 19 nearest, and your position never leaves it. The notification has Done and Dismiss, and a crossing fires at most once an hour. In a browser you can add and change them, and they say Fires on your phone. The iPhone app follows; the Play Store version follows Google's review of background location, so F-Droid and GitHub builds have it first. The API takes kind: "arrive" | "leave" with radius_m, ot task show lists them, and calendar apps never see them (Apple's location alarms stay client data). See Location reminders.

Changed

  • Attachment sizes in an email-to-task description use binary units. The list of attachments read 300.0 KB; it now reads 300.0 KiB (and MiB, GiB…), the unit that matches the 1024 divisor, and keeps scaling past MB.
  • Close, not Done, on buttons that only close. The editors of a task's details (When, Assignee, Take turns, Labels) ended with Done, which on a phone sits right under the task's checkbox and reads as "complete the task" (#801). They now say Close, as do the When editor's own button and the dialog after publishing a task page. On a touch screen, Select in a list now switches to Close, and the selection bar's button that hides the checkboxes says Close. On the Focus screen, a Flowtime session's button that ends the count, beside Complete, now says Stop the timer. Nothing else changes: the buttons do what they did. See Task panel.
  • Location reminders stay out of the Google Play version for now. The Android app from Google Play no longer declares location access (fine, coarse or all the time) and has no location reminders until Google has approved the background-location use; its Settings → Notifications → Location reminders says they come to the Play version later and that the app from GitHub or F-Droid has them. The APK on GitHub and F-Droid is unchanged and still reproducible. CI now builds the Play bundle with -Popentodo.play=true and fails when it declares a location permission or the APK lost one (scripts/android-store-check.sh). See The Android app. (play-location-later)

Fixed

  • Notes and time entries in a shared project stay readable for its members. On an account with encryption on, a note in a shared project and a time entry on a shared project's task were encrypted like personal content, so the project's other members saw Cannot decrypt. Shared projects are never encrypted: these now stay plaintext there like the project's tasks, and the same one-time check writes any of them already encrypted back as plaintext.
  • The unscheduled lane leaves room for the hours on a phone. In the Day calendar on a phone, every task without a time was a full-height card in the lane above the hours. Seven of them took almost the whole screen, so dragging one onto a time was hard (#775). On a touch screen or a narrow window, a lane with more than three tasks now folds behind an Unscheduled header with the count. A tap opens it, and the device remembers your choice. Open, the lane scrolls inside a short area of at most three and a half rows, so the hour grid keeps most of the screen. The folded header still accepts a dropped block. The Week grid on a tablet does the same. The red current-time line, and the ghosts of a drag, no longer paint over the lane when the hours scroll under it, at any width. With a mouse on a wide window the lane looks as before.
  • An answer cut off on the way no longer counts as an empty success. When the connection, or the page, went away while a server answer was still arriving, Safari and the iOS app read the cut-off answer as an empty one, so the app went on as if the request had worked but without its content. Opening a project could then end in an error in the browser console while the project header counted the project's public links. A cut-off answer is now treated like no answer at all: the app reports a network problem, or tries again later where it would after any other failed request.
  • Subtasks go to Later with their parent. Deferring a task with subtasks (giving it a start date in the future, or parking it in Someday) moved the task to its project's Later group but left its subtasks behind in the old group, at the top level with only the parent's name, so they looked cut off (#755). A project's list now keeps each subtree together: the subtasks of a deferred or parked task are listed under it in Later, each with its own dates, and a deferred subtask of an active task stays under its parent with its Starts chip. Display mode's project page does the same. Nothing is written; it is how the list is drawn, offline included.
  • The theme format's JSON Schema is on the docs website. Themes linked /docs/themes/opentodo-theme.schema.json, but the docs build copied only images, so the link was broken on the website. A file under docs/ that a page links is now served beside the pages at that path.
  • Release assets are only the files a release ships. The 0.20.0 GitHub release also carried two of the web app's build chunks, opentodo-dark-*.js and opentodo-light-*.js, picked up because the release job copied every file whose name started with opentodo-. It now copies only the server and ot binaries, the extension and skill zips, the Android APK and AAB and the site bundle, by exact name and from the top of their artifact (scripts/release/collect-assets.sh, tested by make repo-check), and fails when a binary is missing.
  • A failed move of a repeating task's occurrence says so. Dragging a projected occurrence to another day in the Plan, Month, Week or Day dropped any failure silently; it now shows the usual "could not be saved" message, like every other move.
  • Waiting for a slot: a time that has passed says so. In the folded Waiting for a slot group, a task whose time today had already passed looked like one still to come, and the heading could name it as next (follow-up to #766). It now shows its time in red with Time passed, keeps its place and Take a slot, and the heading names the next one still ahead; when every time has passed it says so (time passed: Call the school, 9:00 AM, or 2 past their time). The list follows the clock. ot today marks those lines (time passed) and its --json and today_summary add time_passed.

0.20.0

· On GitHub

OpenTodo 0.20.0 is about routines and themes. Automation rules become routines: a routine has a trigger and steps that find tasks, change them, create tasks or projects and comment, built on a page with a plain sentence for each step, started from a starter or described in one sentence, tried against your data before it runs, with a run history and "Routines since your last visit". A routine can ask AI to judge or summarise, and shows you the proposal before anything changes; a shared project can have its own routines, and encrypted accounts run theirs on one of their devices. The look of the app becomes themes: OpenTodo Light and Dark plus fourteen editor palettes, your own made in an editor, from a file or from a VS Code theme, and a server default for everyone. Also new: Outlook and Microsoft 365 in calendar sync, an Obsidian plugin that sends a line as a task and syncs a folder of task files on a phone too, Upcoming that starts with Today, Today's count on the app icon, and tasks with a time that stay in sight while they wait for a Today slot. The server turns Multipath TCP off (CVE-2026-98432); update the host's kernel too (see Security).

All changes 45

Security

  • Multipath TCP is off. Go turns on Multipath TCP for Linux listeners by default, and CVE-2026-98432, a Linux kernel use-after-free in TCP connection handling, can be reached through MPTCP listeners. OpenTodo gains nothing from MPTCP, so go.mod now sets godebug multipathtcp=0 for the server and ot, pinned by a test. The real fix is a patched kernel: the server, and its container, run on the host's kernel, so update it (advisory, Host kernel). The Go toolchain stays at 1.27.3, which already carries the crypto/x509 fix for CVE-2026-111943.

Upgrade notes

  • Replace the image or the binary and restart; upgrade the server before the devices. Four migrations run at start (0051–0054: calendar sync cursors, routines, routine AI steps, assistant usage drafts); there are no down-migrations, so back up first. New synced entity custom_theme and new synced fields rule.steps, rule.ai_apply, rule.ai_budget, rule.project_id, time_entry.focus_mode, time_entry.focus_session, time_entry.planned_end_at, user_prefs.theme_light, user_prefs.theme_dark, user_prefs.routines_seen_at, user_prefs.routines_hide_successes and user_prefs.upcoming_today, all additive: older devices keep working and ignore them, but a routine with steps runs only on an upgraded server.
  • No new mandatory variable. Outlook calendar sync is optional and needs a Microsoft client, set in Settings or with OPENTODO_MICROSOFT_CLIENT_ID, OPENTODO_MICROSOFT_CLIENT_SECRET (or _FILE) and OPENTODO_MICROSOFT_TENANT (Calendar sync).
  • Update the host's kernel. Multipath TCP is now off in OpenTodo, but the kernel fix for CVE-2026-98432 is the real one (Host kernel).

Added

  • Today limit: tasks with a time stay in sight. With Today full, a task captured as "send the form today by 4" waited in the folded Waiting for a slot group, shown only as a count, so its time could pass unseen (#766). The folded heading now names the next waiting task due today with a time or a deadline, Waiting for a slot · 4 — next: Send the form, 4:00 PM, and lists up to three of them under it, soonest first, each with its time and Take a slot. The slots stay first come, first served: a time never moves anything. ot today prints the same under its Waiting heading and marks time_sensitive in --json; the agent tools' today_summary marks time_sensitive on waiting tasks and lists them in waiting_time_sensitive.
  • Today's count on the app icon. The installed app's icon now shows how many of today's tasks are still open, the same number as Today in the sidebar (all of them, not only the Today limit's places), and goes away when nothing is left (#725). It follows every change and every sync while the app runs; while it is closed, a reminder push sets it to the current day's count from the counts the device stored for the next seven days, and the push itself carries nothing new. It works in the home-screen app on iPhone and iPad (iOS 16.4 or later, once notifications are allowed) and in apps installed from Chrome or Edge; Settings → Notifications → App icon badge turns it off per device and only appears where badges are supported. See App icon badge.
  • Describe a routine in one sentence. Some routines are easier to say than to build: "every Friday at five, make tasks in Errands from my Groceries note" still meant finding four steps and wiring them. Routines → Describe it (and Describe a routine in the command palette) takes one sentence of up to 500 characters and lets the assistant draft the routine: it gets the steps available to you and the names of your projects, sections, labels, saved views and webhooks (as material whose instructions are ignored), answers through one tool, draft_routine, and its draft is type-checked, with one more try carrying the errors. The editor opens the draft unsaved, with Drafted by AI: check each step, then Save and any error on its card; nothing is saved, switched on, tested or run, a draft never turns on Apply without asking, and it never picks a task by hand. It is offered only when you can use the assistant, counts as one turn toward your allowance (listed as routine drafts in your usage; at most 10 an hour), and on an end-to-end encrypted account runs on your device with the on-device assistant, so nothing about it reaches the server. New route POST /api/v1/assistant/routine-draft (sessions only); not in the CLI or MCP, whose assistants already write routines with create_routine (OpenSpec routines-describe-it, #735).
  • Routines that read text, on end-to-end encrypted accounts. With encryption on, the server cannot read a note or a title, so a routine that split a note into tasks, wrote a comment or asked AI was skipped with encrypted. Such a routine now runs on one of your own devices: turn on Run my routines on this device in Settings → Encryption (or from the editor's warning). When the routine is due, the server sends that device the run's ids over the device relay; the device plans the routine on its decrypted copy exactly as the server would (the same planner, pinned to the server's by the shared use cases), and sends back only encrypted changes, which the server applies like any device's edits. Ask AI and Summarise call the provider of the on-device assistant, and their results wait as a proposal encrypted on the device that any of your devices opens and applies. With no device online the run waits (waiting for one of your devices), only the latest occurrence of each routine, and is recorded as missed after 24 hours; Run now runs on the device in hand. The opt-in stays on while the device is locked and ends at sign-out. Email me and webhooks with text stay unavailable. See Routines on encrypted accounts.
  • Routines that belong to a shared project. A household or a small team can now give a shared project its own routines: every Monday, give this week's chores a date, when anyone adds a task to Releases, set it due in three days. New routine asks Where (Just me, or a shared project you edit), and the project's menu has Routines, the Routines page filtered to it. Everyone in the project sees the routine and its runs; editors, admins and the owner change it, viewers read it. It runs as whoever last turned it on (Runs as Alice), with that person's current role and never more than an editor, and it sees only the project: Find tasks, conditions and triggers never reach anyone's personal tasks, Create tasks creates in the project, and a step that would write outside it fails before anything is written. Labels, today's focus, timers, other projects, Email me and webhooks belong to one person, so those steps are marked Not available in a shared routine. Its triggers fire on any member's changes. When someone else edits its steps or AI settings it pauses until someone turns it on again (the editor warns before saving), so nobody's role or AI tokens are used for steps they did not approve; when the person it runs as leaves or becomes a viewer, it pauses and the project's editors are notified. AI steps use that person's assistant and allowance, and Apply without asking counts only when they turned it on. Shared projects are plaintext, so these routines run on the server even for an encrypted account, reading only the project. The CLI creates one with ot routine create --project and lists PROJECT and RUNS AS; AI assistants pass project to create_routine. Email project members follows in #741. (#662)
  • Start a routine from a starter. An empty routine editor is a hard start, and the step catalogue is large. Routines → Start from a starter (and the Routines page while it is empty) now lists eleven ready-made routines, drawn from the routines people build most: Groceries from a note, Sunday overdue sweep, Evening roll-over, Morning focus, Done journal, Note to subtasks, Waiting follow-up, Friday digest by email, Meeting notes to tasks, Inbox over 20 and Monthly review. They ship with the app, work offline and read in your language; search finds them by name, summary or step, in your language or English. A starter's page shows what it does as sentences, what it needs (a step not set up on the server says why) and your choices, the project or label it works with, picked from a list next to the word, with Create Groceries first when you have no such project. Use this starter opens the editor on a new, unsaved routine. Starters follow the rule that routines are dynamic: they read the task that triggered, the task you run them on or tasks an earlier step found, never a task chosen by hand. Each one lives in templates/routines/ beside the project templates, translated into the six languages, checked in CI and run by a Go test against seeded data that must produce exactly the expected writes; anyone can propose one by pull request (templates/README.md). The site bundle publishes them in templates/routines.json with a routineTemplates fact. "Describe it", drafting a routine with AI, moved to its own change (routines-describe-it). (#660)
  • Themes: the server's default and server themes. A family, club or company server already carried its own name and logo; now its look can match. In Settings → Server → Branding the owner chooses the default light and dark theme, from OpenTodo's, the built-in and the server themes, and adds up to 20 server themes, from a .opentodo-theme.json file (Add from file) or as an independent copy of one of their own (Offer one of my themes), each checked on the device and the server (at most 16 KB). Everyone who never chose a theme for a side sees the server's default, and a person's own choice always wins: nothing is written to anyone's data, so changing the default later reaches everyone who still follows it. Each list then starts with Default (Server default: name), the server's default is marked Server default, and server themes appear as From this server, to choose, customise as a copy or export, never to change; choosing OpenTodo's theme while a server default is set now stores it by name, so it keeps winning. A server theme with contrast warnings shows Low contrast · n, and making it a default asks first, listing the pairs. Devices remember the default and the server themes for offline use and the first frame, and read them again when auth/status reports a newer themes.version (at start, and when the app comes back online or to the front). Removing a server theme that is a default clears that default; people who chose it fall back to their side's default and keep their choice. New routes: GET /api/v1/themes/instance for every signed-in person, the owner's PUT /api/v1/settings/themes/default and PUT/DELETE /api/v1/settings/themes/instance/{id}, refused on the demo instance and recorded as settings.changed with ids only. No environment variable; the sign-in screens, emails, splash and icons stay neutral. Deriving a theme from base colours in the middle of the lightness range can take two seconds, so the app now derives a theme at start in a worker, behind the colours the boot script already applied. The first load grew 0.8 KB (Branding, Themes, API, theme-instance-default, #711, closes #638).
  • Routines that ask AI, with your review first. A routine could split, count and move, but not judge: "turn these meeting notes into tasks" or "summarise what I finished this week" needed someone at the keyboard. Routines now have Ask AI (your instruction, up to three values from earlier steps, an answer as text or lines) and Summarise, which call your assistant's provider once each, without tools, with what they read marked as material whose instructions are ignored. A run of a routine with an AI step writes nothing by itself: its changes wait as a proposal in the Inbox card (Sort inbox · proposal waiting · Review) and the bell, reviewed item by item with Written by AI on text the AI wrote, applied in one tap with one Undo, and expire after 7 days. Apply without asking, off by default and only switched on by you in the app (an AI assistant, MCP tool or agent token never can), lets a routine create tasks, comments and labels and set task fields at once, at most 20 items a run, always listed in the Inbox with Undo; it turns itself off after a run that would do more, or after two failed runs in a row. Each routine has a daily AI budget (20,000 tokens by default), the card previews never call the model, and Test says which provider gets the inputs and about how many tokens it uses before sending anything. AI steps need the assistant's updated terms (Accept for routines in Settings → Integrations → Assistant). See Routines.
  • Themes: your own, made in an editor, from a file or from a VS Code theme. Settings → Appearance → Theme and density gains Your themes: up to 20 themes of your own, synced to your devices (never to anyone else's) through the new personal theme entity, offline included, and part of the export. Every theme card has a menu: Customise opens an exact copy credited to the original ("Based on Nord by Sven Greb (MIT)"), and your own themes add Use for light or Use for dark, Edit, Duplicate, Export and Delete (a theme in use goes back to OpenTodo's in the same change, with Undo). The editor (/settings/appearance/themes/<id>) has the four base colours and the optional status colours, each with swatches, a hex field and keyboard-operable lightness, chroma and hue sliders; Advanced, with every colour of the theme by group, Derived or Set, its worst contrast pair, Undo and Reset, and a filter in your language or English; corners, the font (an installed font is never downloaded) and the list text size; and a live preview of a list, a board, the week and a task panel built from sample data. Contrast lists every pair below AA with Fix and Fix all, which keep each colour's hue and move only its lightness, and a screen reader hears the results. A theme with warnings can still be saved; its card reads Low contrast · n. Changes stay a draft on the device until Save, with undo and redo for the session (Ctrl+Z, Ctrl+Shift+Z, Ctrl+S in the editor). Themes travel as .opentodo-theme.json files, read and checked on the device with line-precise errors, and Import a VS Code theme maps a VS Code colour theme's workbench colours onto the theme (the full table is in the docs), derives the rest and shows a report first, with Fix contrast now ticked. Themes are not encrypted, and the editor says so on encrypted accounts. The editor, its primitives, the importer and the derivation worker load only when used; the first load grew 1.8 KB, mostly chunk overhead (Themes, theme-editor, #710, part of #638).
  • The routine editor, run history and "Routines since your last visit". Routines could only be built through the API, the CLI or MCP, and a run that failed at night went unnoticed. More → Routines (the old Rules page; /rules opens /routines) lists each routine with its trigger and steps as one line, its last run and runs this week, how many steps are not available, Run now and a menu, and a Runs tab with every run. The editor shows a routine as cards stacked one under the other, phone first: a When card and one card per step, each a sentence whose bold words are buttons (Create tasks from the lines in Errands) that open the right picker: the Combobox for projects, labels, tasks, views and webhooks, fields for text (with an Insert menu of earlier outputs), dates and numbers, the rule condition and action controls. Each card ends with its output, offered as a chip to later steps (fitting ones first, others under Needs conversion with the step that converts them); If and Repeat for each show their steps indented. Every card previews what it would do while you edit, worked out on the device for text, lists, logic, Find tasks by a condition and the writing steps' effects (offline and encrypted accounts included) and by the server for the rest (Would write 620 of 500 warns of a limit); a preview never writes or sends anything. A wrong kind of value shows on its card with a one-tap fix (Notify me expects Text, got Tasks · Insert Format text), and Save waits until it is fixed; Ctrl+Z takes an edit back. + Add step searches every step by name, description and synonyms in your language and English, suggests the steps that fit the step above, and shows a step that is not set up dimmed with what to set up and a link; a saved step that stops being available reads Not available with the reason. Test runs the draft as a test, Save works offline, and each routine has a History tab; a run opens step by step with the tasks it touched (titles from your device), why it stopped and Undo run. A one-card rule keeps the rule form, so older versions still edit it. The keyboard covers it all: Alt+↑/Alt+↓ move a step (focus stays and the move is announced), a adds one below, ←/→ move between words, Ctrl+Enter saves, Ctrl+Shift+Enter tests; every card and word has a screen-reader name. The Inbox shows a card Routines since your last visit above the first task when something happened: one line per routine (at most five, then See all runs), runs you started yourself left out, failures until handled, successes until seen (after two seconds on screen or Mark as seen, on every device through the synced user_prefs.routines_seen_at) or dismissed, and Hide successful runs in the card's menu and Settings → Notifications (user_prefs.routines_hide_successes). A failing routine also rings the bell once a day per routine with a count (routine_failed) and reaches delivery channels with Routine notices on; a failed run is now kept until seen and 30 days more, at most 90. New routes GET /api/v1/me/rules/runs?for=inbox and POST /api/v1/rules/{id}/runs/{run}/dismiss; links to a run read /routines/<id>?run=<run>. The page and editor are one lazy chunk (28.5 KB gzip) and the Inbox card loads only for users with routines; the first load grew about 1 KB. (Routines, API, routines-editor, closes #659)
  • Themes: fourteen built-in editor palettes. Next to OpenTodo Light and Dark, Settings → Appearance → Theme and density → Theme now offers Solarized Light and Dark, Gruvbox Light and Dark, Catppuccin Latte and Mocha, Rosé Pine Dawn and Rosé Pine, Tokyo Night Light and Tokyo Night, One Light and One Dark, and, dark only as their projects publish them, Nord and Dracula. Each palette is mapped onto the app's colours by one written mapping and passes the same WCAG AA contrast check as every theme; where a published colour was too faint for its place (most often a comment colour used for secondary text), the theme keeps its hue at the nearest lightness that reads, records the published value, and says so in its details. Under System, choosing one side of a pair offers the other (Also use Solarized Dark at night). Every card credits the palette's author, About this theme shows its licence, copyright line and project, and the full MIT notices ship with the app and in the docs; OpenTodo is not affiliated with these projects. The palettes load with Settings, never with the first screen, and set colours only, so switching never moves the layout. GitHub's Primer, Monokai and Material Theme were left out over their names and licences (Themes, #709, part of #638).
  • Themes: the engine, with OpenTodo Light and OpenTodo Dark as its first two themes. The look of the app was fixed apart from light or dark, density, high contrast and the accent, and #638 asked for more, the way code editors do it. A theme is now a small, documented JSON file (format 1, a JSON Schema in docs/themes/) of a few seeds, canvas, surface, text and accent, and optional values for any of the app's semantic colours, its corner radii and its typeface; every colour a theme leaves out is derived on the device, keeping each seed's hue and moving only lightness until every contrast pair passes, so a theme of four colours is complete and stays readable. Settings → Appearance → Theme and density gains a Theme row: one theme for light and one for dark (both lists under System, which switches between them with the device), synced like the other appearance choices and part of the export. The theme applies before the first frame with no flash, high contrast always wins over it, and the browser bar and the Android app's status and navigation bars follow it. This version ships only the OpenTodo themes, which look exactly as before (the accent row is now called Accent); the built-in palettes, your own themes and the owner's default theme follow. Every theme in the repository and 200 seed-only themes must pass the contrast check in make check. There is deliberately no custom CSS; Themes explains why (#708, part of #638).
  • Routines: rules with steps. An automation rule could change the task that triggered it and nothing more, so "every Friday, turn the lines of my Groceries note into tasks in Errands" or "count what waits, and tell me when it is more than ten" were out of reach. A rule can now hold steps, each using what the steps before it produced: find tasks (by a condition, a filter query or a saved view, evaluated on the server by a port of the app's filter evaluator), create tasks (one per line, read like quick add), change tasks, comment, read and append to or clear a task's or project's note, split into lines, join, format, find and replace, filter, sort, take, count, If/Otherwise, Repeat for each, Stop, start and stop the timer, tracked time, set today's focus, create projects, sections and labels or a project from the template library, and Notify me (in the app and, with the new Routine notices event, through your delivery channels), Send a webhook (one of yours, with a routine object in rule.fired) and Email me (your own verified address only, at most 10 a routine and 20 an account a day, never for an encrypted account). Every existing rule keeps working as a one-step routine; nothing is converted. A step is offered only when what it needs is set up, and a step that stops being available fails the run at that step with the reason. A run plans every step first and writes only when all of them planned, one batch per item as the routine; limits are 20 steps, 60 seconds, 500 items written, lists of 500 and texts of 64 KB. Routines can run on a new manual trigger and be run now or tested from the command palette (Run routine…, with the open task), POST /api/v1/rules/{id}/run, ot routine run or the MCP tool run_routine; every run is logged per step with ids and counts only, and Undo takes a whole run back. New routes: /api/v1/rules/test, …/{id}/run, …/{id}/runs/{run}, …/runs/{run}/undo, /api/v1/me/rules/runs, /api/v1/me/rules/catalogue; ot routine lists, shows, runs, creates and edits routines from JSON or YAML with names resolved, and exits 6 on a failed run; MCP gains list_routines, get_routine, list_routine_runs, get_routine_run, the resources opentodo://routines(/{id}), and behind the write gate create_routine, update_routine, set_routine_enabled, run_routine and trash_routine. A routine with a text step is skipped for an encrypted account until device runs arrive. The history now labels a rule's changes Routine (name). Synced as the additive rule.steps and trigger kind manual; one migration adds run records; the routine editor comes next (routines-editor). (Routines, API, CLI, MCP, routines-engine, closes #658)
  • Speak a brain dump or photograph your notes, and review the tasks. Dictation filled quick add with one task, so a list spoken on the way back from a meeting or written on a whiteboard still had to be typed task by task. The assistant's message box now has a microphone and Add photo, and a new skill, Turn this into tasks, proposes the whole list as one change set: one task per action in your words, a project, label, date, priority or estimate only where the material says so, and a change to an existing task ("already in your tasks") instead of a duplicate; it never trashes anything, and a capture is always reviewed, never applied automatically. The microphone uses the recogniser voice capture already uses on the device (the browser, or the server's speech service in pieces of at most 55 seconds) in a continuous mode that stops on the button, Esc, 8 seconds of silence or 5 minutes ("Listening 1:24 / 5:00"); the text lands in the box, editable and unsent, without quick-add tokens. Up to 4 photos (also pasted or dropped) are turned upright, scaled to 1568 px and re-encoded as JPEG without location on the device; the first one on a device asks once, naming the provider; the conversation keeps "2 photos (not kept)" and never resends them. The command palette's Ramble into tasks opens the assistant with the skill and the microphone listening; there is no new key or sidebar entry, and none of it appears without an assistant. POST /api/v1/assistant/turn accepts images on the last message (at most 4 of 3.75 MB, 16 MB per request, the text still 256 KB), held in memory for the turn only and never logged or stored; Anthropic gets image blocks, OpenAI and Gemini image parts, a compatible endpoint only when the owner ticks This model reads images (providers.compatible.images; Test connection then sends a small image), and a model that refuses photos answers images_unsupported with the message and photos kept in the box. GET /api/v1/me/assistant reports images; the disclosure (version 2) adds "photos you add" while earlier consents stay valid. Encrypted accounts get the same on the device, which sends photos straight to its own provider. capture is also an MCP prompt (text only). A new provider test sends a Gemini request with tools, streaming and a photo to Google's OpenAI-compatible path, and a redirect in front of Test connection is shown to be refused before Gemini is asked (refs #501). (Speak or photograph a list, API, voice-ramble, closes #608)
  • The running Focus session, readable outside the app. A "now" panel beside the screen (a small web page, a Home Assistant card, an e-ink display) could see at most that a time entry ran, so it kept its own timer and drifted from the app. The time entry a Focus work session writes now carries the timer (pomodoro, flowtime or countdown), the session number in the Pomodoro cycle and when the stretch is planned to end (none for Flowtime); a resume after a pause writes the new end, and "+5 min" starts an entry with its own. The new GET /api/v1/time-entries/running answers your own running entry with those details as focus: {mode, session, plannedEndAt}, or null, with serverTime, for every token scope including read; the list routes and the JSON export carry the same, the CSV export is unchanged. ot time status prints it ("Pomodoro, session 3, ends 10:25"), MCP's list_time_entries gains focus per entry, and the resource opentodo://time-entries/running holds the running entry (null for a task in a project kept away from assistants). Breaks and pauses write no entry, so they read as nothing running; the phase, the pause bookkeeping and the breaks stay on the device. Synced as the additive time_entry.focus_mode, focus_session (0 to 99) and planned_end_at (never before started_at, refused with reason invalid_planned_end), written only by the entry's author; older devices ignore them. (Show Focus elsewhere, API, Home Assistant, focus-session-readable, closes #604)
  • AI assistants finish the job. An assistant planning the day through an agent token could create and schedule tasks but had to hand back everything around them: a reminder for the 10:00 block, the client's project note, a section or a project move over REST, custom field values over REST, archiving a finished project, renaming a label, logging the Pomodoro it worked beside you. New MCP write tools do all of it under the same switches, limits (50 items a call, 30 calls a minute, 1,000 items a day), agent.write audit, history label and "kept away" projects as every assistant write: add_reminders, update_reminders and remove_reminders (relative to the due time, before the deadline or at a fixed moment, fire_at computed in your time zone; your own reminders only, at most 10 on a task; get_task lists them), update_project with description or append_description for the note (editors may, viewers not), archive_projects (with the sub-projects, owner or admin, one project a call), update_labels, and start_timer, stop_timer, log_time and update_time_entries. Starting never silently stops a timer you run: it answers timer_running unless the call says stop_running: true, and an assistant can change only the time entries it created and never delete one. The agent REST routes gain the same: fields on tasks (the spec promised it; the server refused it), PATCH /api/v1/projects/{id} with icon, parent_id, description or archived on its own, POST/PATCH /api/v1/sections, PATCH /api/v1/labels/{id}, POST/PATCH/DELETE /api/v1/reminders, and POST /api/v1/time-entries, …/stop and PATCH …/{id} (409 timer_running unless stopRunning: true); ot project archive and ot time now work with an agent token. Deleting sections, labels, projects or time entries stays refused. On an encrypted account the note, label names and entry notes take ciphertext only, and append_description works through an opted-in device or ot mcp --decrypt. No new synced field, no migration. (MCP write tools, Agent tokens, agent-tools-complete, closes #565)
  • Outlook and Microsoft 365 in calendar sync. Outlook.com, Hotmail and Live accounts and Microsoft 365 work or school accounts offer no CalDAV, so calendar sync could not reach the calendar most office workers live in. With a Microsoft client set up by the server owner, Connect Microsoft sits next to Connect CalDAV and Connect Google in Settings → Integrations → Calendar sync (stacked full-width on a phone): Microsoft asks for the account and the permission, and the connection works like any other, with the same card, states, limits (3 accounts, 200 writes a check, 10 shown calendars), newest-change-wins rule, encrypted-account rule and "calendar sync" in a task's history. Underneath, the cycle now runs over one transport interface, WebDAV for CalDAV and Google and Microsoft Graph for Outlook: calendarView/delta per calendar with its delta link kept in a new side table and rebuilt from a full listing when the window moves or Graph answers 410; events created with an OpenTodo extended property holding the task id and a transactionId (a retried creation makes no second event), immutable ids, and PATCH/DELETE with If-Match (a 412 is never forced); only events carrying the property change tasks. A zoned task is written in its zone's Windows name, a floating or all-day task in the mailbox's time zone (UTC, with a note on the card, when Microsoft does not say), so wall-clock times round-trip. A 429 or 503 waits at least Graph's Retry-After; a revoked grant asks to reconnect; a refused or expired client secret stops every Microsoft connection with microsoft_client_refused, shows the owner when it happened, and saving a new secret resumes them at once; a target that is gone or can no longer be written stops with calendar_missing or calendar_read_only. Refresh tokens, which Microsoft rotates, are stored before the access token is used. The owner configures the app registration's client id, tenant (common by default, organizations, consumers, a tenant id or a domain) and secret in the panel, or with the optional OPENTODO_MICROSOFT_CLIENT_ID, OPENTODO_MICROSOFT_CLIENT_SECRET (or _FILE) and OPENTODO_MICROSOFT_TENANT; the guide walks through the Entra ID app registration. login.microsoftonline.com and graph.microsoft.com are contacted only once a client is set up and someone connects, through the outbound policy and without redirects. New routes GET/PUT /api/v1/settings/calendar-sync/microsoft, POST /api/v1/calendar-sync/microsoft/start and GET /api/v1/calendar-sync/microsoft/callback, all refused on a demo instance. One additive migration (calsync_cursors), no op schema change, no new dependency. (Calendar sync, API, outlook-calendar-sync, closes #609)
  • A reproducible Android app with one signature, ready for Google Play and F-Droid. The Android app was only a signed APK on the GitHub release, and nobody could check that it was built from this source. Building a release tag now gives the same APK apart from the signature, whatever the machine, path, time or Gradle cache: the toolchain is pinned (Temurin JDK 17.0.21+9, the Gradle wrapper with its checksum, Android Gradle Plugin 9.4.1, build tools 36.0.0) and the APK no longer carries the encrypted dependency block or the commit file. Every release tag builds the app a second time in a fresh checkout and fails before anything is published when the two differ; scripts/android-repro.sh v<version> opentodo-android-<version>.apk lets anyone compare a downloaded APK with a build of its tag (with apksigcopier, pinned by hash), and Actions → Android reproducible build runs the check on any commit. The release certificate's SHA-256 is published in the guide and checked by CI on every signed build, so the APK from GitHub, F-Droid and Google Play can carry one certificate and you can switch between them without uninstalling. The F-Droid metadata (mobile/android/store/fdroid/app.opentodo.yml) now points F-Droid at the release APK and that certificate, so F-Droid publishes the project-signed APK only when its own build matches. A final tag also uploads the app bundle to Google Play's internal testing track with the release notes (scripts/release/play-upload.mjs, the Play Developer API with no dependency; release candidates upload nothing and nothing is ever promoted to production by CI); without the publishing secrets the step warns and the release goes on. The store listings open once the owner's Play Console and F-Droid steps are done; until then, install from GitHub. No change to the app itself; the APK stays about 1 MB. (The Android app, mobile/README.md, play-production, refs #603)
  • Tasks that link back to an Obsidian note. A task written from a line in an Obsidian daily or meeting note had no way to point back at that note: origins were web links only. A task's origin can now be an Obsidian note ({kind: "obsidian", id: "Daily/2026-10-29.md", url: "obsidian://open?vault=Notes&file=Daily%2F2026-10-29"}), set through the API, an AI agent or the coming Obsidian plugin. Its chip shows a note icon and the note's name, its tooltip "Open in Obsidian: Notes › Daily/2026-10-29.md", and activating it opens the note in Obsidian (the browser asks first); origin: obsidian lists these tasks in a filter. Only that one link shape is accepted, on the server and again on each device after decryption: another Obsidian action (new, search, a plugin's own command), a path= link or extra members are refused, and the link must open the note the origin names. The Android and iPhone apps hand the link to Obsidian through their external-link method behind a new bridge capability, obsidian-links; an older app shows the chip with Copy link instead. No new field, migration or variable: an older server refuses the new kind, so writers check the version first (#605).
  • Sync a folder of task files from Obsidian, on a phone too. ot vault kept a folder of Obsidian Tasks files in two-way sync, but only where a Go binary runs, so people with Obsidian only on an iPhone, an iPad or an Android phone had no vault sync. The OpenTodo plugin for Obsidian now does what ot vault sync does, inside Obsidian: Settings → OpenTodo → Sync a folder with OpenTodo picks a folder (default OpenTodo/), and the plugin writes one file per project and one line per task, merges edits from both sides field by field and keeps lines that changed on both sides in .conflict.md files, with the same files, limits and .opentodo/state.json as ot vault, so a folder moves between the two. It syncs when the plugin starts, a second after a file in the folder changes, every 30 seconds while Obsidian is in front and with Sync vault with OpenTodo now; never in the background. Its engine is a TypeScript port of internal/vault, kept in step by step-by-step sync vectors that the Go engine writes and both engines run (internal/vault/testdata/sync/). A folder now has one syncing device: the state file names it, with a five-minute lease every sync renews, so a folder that Obsidian Sync, iCloud or git copies to several devices is no longer synced by two of them pushing each other's edits back. The others show which device syncs it and offer Take over syncing here, a fresh first sync that keeps the server's values and lists differing lines in conflict files; on the command line ot vault sync and watch refuse such a folder before any request with vault_owned (exit 2) and gain --take-over. A file iCloud has not downloaded yet (a .<name>.icloud placeholder) is skipped instead of being read as deleted. Encrypted accounts work with the plugin's unlock, after a notice that the folder holds plain text. The plugin's main.js budget is 400 KB (258 KB now); guide: docs/obsidian.md#sync-a-folder (#607).
  • Send a line from Obsidian to OpenTodo. Tasks jotted in daily and meeting notes stayed there, and Obsidian on a phone could not run ot vault at all. The new OpenTodo plugin for Obsidian (desktop and mobile) sends the line at the cursor, or each line of a selection, as a task: a Tasks checkbox line (- [ ] Renew passport 📅 2026-11-05 ⏫ #errand) is read with the vault's grammar, any other line with quick add (Call the plumber tomorrow 9am p2). A sheet shows what was recognised and the project before sending; afterwards the line gains [opentodo:: <link to the task>] (or is ticked or removed), Undo stays ten seconds, and the task's origin opens the note again, following renames. Sends wait on the device while offline, and encrypted accounts are encrypted on the device with the web app's own code. It connects with a full token kept on the device, never in the vault, talks only to your server and collects nothing. On the server, the sync exchange gains an optional push_only feature that applies a device's ops without pulling the log. The plugin lives in obsidian/, is built and tested by make obsidian and make obsidian-e2e, and each release publishes it to OpenTodoApp/obsidian-opentodo; guide: docs/obsidian.md (#606).
  • Start Upcoming with Today. Planning now and next meant switching between Today and Upcoming, or a saved view that only came close (#670, thanks @calli33). Start with Today in Upcoming's ⋯ menu, off by default, makes Upcoming open with Today's own groups, picked and grouped exactly as Today does it: Earlier, Today (with Morning and Afternoon when day parts are on), This Evening, Blocked and, with a Today limit, the folded Waiting for a slot group; then Tomorrow and the following days as before. Each task is listed once and the Today part holds the same tasks as Today, so the numbers agree; Done today and Close the day stay on Today, and Today itself doesn't change. The choice is one synced preference of the account (user_prefs.upcoming_today). ot upcoming and the agent tools keep listing the days after today. See Start Upcoming with Today.

Changed

  • The Obsidian plugin, ready for Obsidian's community directory. Obsidian now reviews plugins automatically at community.obsidian.md (pull requests to obsidianmd/obsidian-releases ended in May 2026), and its review rebuilds the plugin from the release repository. Each release therefore also publishes the plugin's source to OpenTodoApp/obsidian-opentodo, where npm ci && npm run build gives the released main.js byte for byte, and CI runs the directory's checks first (make obsidian-review: build verification, dependency audit, type-check and Obsidian's own ESLint rules). Following Obsidian's guidelines, the commands no longer repeat the plugin's name, which Obsidian already shows (OpenTodo: Send line as a task, Send pending tasks, Fix task links in this note, Unlock encrypted account, Sync folder now; the editor menu keeps Send line to OpenTodo), the settings tab starts without a heading and calls its last section Folder sync, and files the folder sync removes go where Obsidian's "Deleted files" setting says. The submission is prepared in obsidian/store/submission.md. See Obsidian plugin.
  • Theme settings that use the space, and a theme template. The theme editor was a narrow column of full-width rows beside a preview that left most of a wide screen empty, and at 1024 px the preview squeezed task titles to one letter. It now lays out by the room it has: from about 830 px the controls sit beside a preview that stays in view, with the base and status colours as compact rows two to a line (swatch, name, Derived or Set, a hex field you can type in, Reset), a contrast warning on the row of each colour that fails (2 pairs below AA) as well as in the Contrast panel, labels above Corners, Font, List text size and Kind, and Advanced folded with its counts; narrower, the preview is a strip above the controls. In Settings → Appearance, the buttons under Your themes touched the next setting; the row now has the spacing of the others, a short explanation with Learn more, an empty state, and one row of actions, New theme, Import (theme file or VS Code theme) and the new Download a template: a starter file made on your device from the theme you use, with every colour of the format and a note on each in your language, $schema for editors, and the colours you do not set as lines to uncomment. Theme files may now hold such notes and trailing commas; the editor's menu offers Download as a template, and Server → Branding offers it beside Add from file, whose list no longer touches its divider either. Your own themes' cards show their label colours. The layout check now fails when controls overlap or touch a row divider on these pages.
  • No dialogs in the routine editor. + Add step covered the page with a dialog, and the trigger word (Only when I run it) opened a form hanging off its left edge over the sidebar. Now the list of steps expands right where you pressed + Add step (under the last card, inside a Then, Otherwise or Repeat block, or between two cards for Add step below), with its search focused, groups, arrow keys and Enter, and Escape closes it; the new step appears in that place. The trigger word opens the list of triggers right under it, at its start edge, like every other word, and a schedule's time and days (or a trigger's label or project) are set inside the When card, as are the When card's conditions. Delete step removes the step at once with Undo in a toast instead of asking. Test with AI steps asks in the Test panel, and Describe it opens its field on the Routines page under the header. See Routines.
  • Told once that the assistant's list is complete. Since the consent list became complete (below), people who had accepted the earlier, shorter one kept using the assistant without seeing it. They are still not asked again, and nothing pauses, but the assistant panel, and the Routines page when their consent covers AI steps, now show a notice once: Review the list opens the complete list in place, Turn off the assistant withdraws the consent, and Got it closes the notice on every device. The same goes for the on-device consent of an encrypted account. GET /api/v1/me/assistant reports disclosure_notice and consent.seen_version; POST /api/v1/me/assistant/disclosure-seen records Got it. (Assistant settings, API, assistant-disclosure-notice, follows #762)
  • No gap before a colon in a routine's sentence. Comment on [each task] : text had a space before the colon, as did Change [the tasks] :, Favourite [project] :, Email me [subject] : and In [text] , replace: a word followed by punctuation no longer keeps its trailing margin.
  • The routine editor's pickers, polished. Pick's mode menu hung off its word over the sidebar with a solid green pill for the chosen mode, a short word such as all sat in a wide box, a comment was cut to one line with its values read as «each task», the / list covered the step, a step you had just added already said needs a value here, and a second Escape threw away a typed comment. Now every word's list opens at the word's start edge (the / list at the cursor, + value at its button), below it when there is room, with the chosen entry a row with a check; words are as wide as their word; a text shows whole, wrapped, with its values as chips; needs a value here waits until you move on or press Save; and Escape keeps what you typed. The condition rows line up in one grid, the phone sheet keeps Done in reach, and the condition reads due earlier instead of due: due earlier (Routines, routines-editor-polish).
  • Routines work on what they find, not on tasks you pick by hand. Building "find my overdue tasks, then comment on the first" made the editor insert Get the task [choose a task], and inside Repeat for each a comment still asked for a task. Now every step that acts on tasks takes the tasks that flow to it and acts on each: a step you add is already wired to the step above, the Repeat's item (each task) or the trigger (the task that triggered), and fixes use what flows there or insert Find tasks…, never a task picker. Take first became Pick: Pick the first of the tasks, and its first word switches to the last, the first N, the last N, all, or the ones where a condition holds, with Sort first when the order matters. A task chosen by hand is still possible for the rare fixed target, folded under A specific task… and Get a specific task, and routines saved with one keep working. The pickers no longer open a large box: a choice opens a short searchable list next to the word (arrows, Enter, Escape), a text is edited right in the card with / or + value to insert a value, and condition rows open inside the card on a wide screen and from the bottom of the screen below 1024 px. New, so more routines can be built: a project's note chosen directly, Get the date, Get the project of a task, subtasks of a task, Split into lines with only the open checkboxes, Find tasks with a query that names completed (completed = this week), the condition Not changed for N days (unchanged) and schedules on days of the month (month_days, with Last day). A suite of 31 realistic routines runs against seeded data on every build and the most common ones are built through the editor in tests. ot routine show prints Pick by its mode and the MCP routine tools describe the contract. No new synced field (Routines, API, routines-dynamic-data, closes #722).
  • The routine editor is a page. In a dialog, a routine of several steps, its test result and its History were squeezed into one scrolling box, a run's detail opened as a second dialog on top, and the editor could not be linked or reloaded. A routine now opens at /routines/<id>, and a new one at /routines/new. Both work from a link and on reload, and /rules/... addresses redirect there. Opening a routine from a view or a board column opens the same page with the draft filled in. The sticky header shows the breadcrumb Routines › name, Unsaved changes, Test, Run now and Save; on a phone these actions sit in a bar at the bottom. On a computer the steps sit in a centred reading column, with the test result, the routine's History and a run's detail (/routines/<id>?run=<run>) beside them, inline. On narrower screens, tabs switch between Steps, Test and History. Save keeps you on the page, and saving a new routine moves the address to its id. Leaving with unsaved changes asks first, whether you use Back, the breadcrumb, Esc, another page or the browser's Back, and closing the tab brings up the browser's prompt. Ctrl+S now saves too. Focus moves to the page heading when the page opens and returns to the routine's row on Back. A save message such as A routine needs at least one step now goes away as soon as you edit, instead of staying after the steps were added. The layout check now covers the Routines page and the editor at every width. Web only: no op, route or server change (Routines, routines-editor-page).

Fixed

  • OPENTODO_PORT changes the port the process listens on. Setting it on the container was ignored, because the image always sets OPENTODO_ADDR=:8080, so a published port such as 7610 never reached the app. The server now listens on OPENTODO_PORT when that variable is set, and the health check probes the same port. In .env next to Compose the name still only changes the published host port and is not passed into the container (#877).
  • The assistant's consent lists everything it may send, in your language. The list you accept before your first message had fallen behind the assistant: it left out notes, tracked time, routines and calendar frames, workspaces, custom fields, locations, the names of attachments, and the saved view names and webhook hosts a Describe it draft uses. The routines paragraph now also says that an Ask AI step sends its instruction and can read a whole project. On an encrypted account the on-device assistant shows its own, shorter list, and every entry is translated instead of shown in English. Nothing that is sent changed, so consents you already gave still count (What the assistant sends and keeps).
  • A restored attachment and an upload can no longer share the last room in the quota. Restoring a removed attachment checked the quota against stored files only, so a restore that arrived while an upload was still in progress could take an account over its quota by one file. A restore now counts the uploads in progress, and an upload counts a restore that is still being saved; whichever comes second is refused with quota_exceeded (#676).
  • ot vault watch no longer reads a file while an editor is saving it. Many editors save in two steps, an empty file and then its text. When a change from the app arrived between them, the watch synced at once, read the empty file as every line removed, moved its tasks to the trash and wrote the file over the save. A change from the app now waits, like a saved file already did, until the files have held still for a second.
  • A vault line with both [evening:: true] and [daypart:: …] reads the same after a sync. The vault and the Obsidian plugin kept the part when reading the line, but printed only the evening flag, so the task's line changed on the next write. Evening wins when reading too, as the docs say (found by the nightly fuzzer, #679).
  • Long task titles use the room beside them. In a window that is not full width, list rows kept about 9 rem free at their right end, on every row and at all times, for the actions that appear on hover, and cut the title to one line before it, so titles were truncated next to an empty area. Titles now use the row's whole width, wrap onto a second line and end with an ellipsis after two; resting the pointer on a cut title shows it in full, and a screen reader always hears all of it. The hover actions float over the row's end, so hovering a row moves nothing and never changes its height. On a very wide list a title wraps at about 100 characters, and the date and the other columns stay on its first line. Phone rows keep one line. The Inbox also no longer keeps an empty No task selected pane beside the list below 1440 px (zooming out to see more brought it in at 1024 px): there the list takes the whole width and the pane appears only while a task is selected (task-list-width, #650).
  • Typing on an iPhone no longer zooms the app in. Safari on iOS zooms the page when a text field with text smaller than 16 px gets focus, and the installed app stayed zoomed after the keyboard closed, so parts of the screen hung off the edge. On touch screens every text field, select and editable area (quick add, the task editor, comments, search, the command palette, the routine editor, pickers and the notes editor) now uses at least 16 px text; the desktop design is unchanged and pinch zoom stays on. When the keyboard closes, a document Safari left scrolled goes back into place. The layout check now fails on any field under 16 px in its touch emulation (#724).
  • Events from your other calendars in Week, Month and Year. Events of subscribed and connected calendars showed in Day and the Plan, but not on a phone's Week, nor in Month or Year (#733). The phone's Week now lists them in each day: all-day events after the multi-day tasks, timed ones among the timed tasks at their start. Month lists a day's events after its tasks (all-day first, then by time), inside +N more so tasks keep the first rows; on a phone a small ring marks a day with events and the day's list under the grid shows them. Year marks a day with events with a small hollow ring, apart from the density bar, which still counts tasks only; the day's name and tip say how many. Month and Year get the Calendars button and the "Calendars as of" line, and all views follow it and show only enabled calendars; events stay read-only. A device now fetches a year or several months of events in requests of at most 92 days (OpenSpec change calendar-events-all-views).
  • The start guide's "How do you want to work?" step fits its card. The guide is a narrow card, but its three ways of working went side by side by the window's width, so in a wide window they were squeezed and long words (in German, "Methodenhinweisen") ran past the edge. The two offers that follow choosing the full Open Loops method kept their buttons beside the text, so the question shrank to a word a line and a button ended up outside the card, at every width. The cards now go side by side only when the chooser itself has room, which they still do in Settings, and an offer's buttons, like those of every notice with actions, move under the text when there is no room beside it. The layout check now opens each step of the guide with each way of working chosen, in every language it runs, and fails when content sticks out of the card or a sentence is squeezed beside its buttons (#731).
  • Routines no longer find tasks of archived projects or of projects in the Trash. A routine's Find tasks (by a condition, a filter query or a saved view) read every task not itself in the Trash, so it also found the tasks of archived projects and of projects moved to the Trash, which no list in the app shows: a saved view listed more in a routine than on its own page, and a Sunday sweep commented on tasks archived away. Find tasks, the matching tasks of a routine or rule on a schedule, the card previews and the When card's count now follow the app's own rule: never a task in the Trash or of a project in the Trash, and tasks of archived projects only when the Find tasks card has Archived projects on (added from its menu; the additive archived input in the API, ot routine and MCP). Only the task's own project counts; completed tasks keep their rule. (Routines, routines-find-scope, closes #734)
  • A busy day in the Month no longer pushes the grid sideways. A day cell showed its planned load ("3h 45m / 8h") on one line beside the date, wider than a cell on a tablet or a narrow window, so it ran over the next day and, in the last column, made the whole Month scroll sideways. The load now moves under the date when the two do not fit and is cut short inside its cell, with the full figure on hover.

0.19.0

· On GitHub

OpenTodo 0.19.0 is about repeating tasks, the calendar and notes. A repeating task now shows its coming occurrences in Day, Week and Month; any one occurrence can be completed early, skipped, moved or edited alone; a series can change from one date onward without losing its history or a habit's streak; and All occurrences can fix the past ones too. Tasks can span days, the Month can stack several months and a new Year view shows the whole year at a glance. A new Notes page gathers task notes, project notes and standalone notes with search on the device, notes reach the server after a pause instead of after every keystroke pause, and the server removes old note versions so the database stops growing. Board columns fold and cards can show their notes, you can pick your own accent colour, and the Inbox shows only the Inbox. There is no migration; an older database is rewritten once in the background when the server is quiet (see the upgrade notes).

All changes 23

Highlights

  • Repeating tasks. Future occurrences in Day, Week, Month and optionally Upcoming (#550); act on one occurrence alone, with CalDAV exceptions (#551); This and following, with history, streaks and for N times counts carried across (#552; thanks to @pieros7, whose request #537 started this work); and Only future ones or Also past ones under All occurrences.
  • Calendar. Tasks that span days, drawn as bars from start to due and dragged by either end (#558); Months shown 2, 3 or 6 in the Month and a Year view (#557).
  • Notes. A Notes page with standalone notes, note search, ot notes, a REST collection and MCP tools (#655); a Formatting button beside notes on a computer (#654); notes sent after a pause (#653); compaction of superseded note versions, with a Database storage card for the owner (#652).
  • Boards and appearance. Fold a board column and show notes on cards (#561); a Custom accent colour with contrast guaranteed in light and dark (#563).

Upgrade notes

  • Replace the image or the binary and restart; upgrade the server before the devices. No migration and no new variable. New synced entity note (standalone notes) and new synced fields task.span, task.occurrence_date, task.split_from, the appends.<id> family of a task's or a note's notes, view.card_notes, project_prefs.card_notes, user_prefs.theme_accent_custom and user_prefs.formatting_toggle, all additive: older devices keep working and ignore them, but only an upgraded server stores and serves them. Back up first; there are no down-migrations.
  • An older database is rewritten once so it can shrink. A database created before this version is converted to SQLite's incremental vacuum mode by one full VACUUM, run by the new compaction job at a quiet moment (no change for 10 minutes, or at its next run once 7 days passed) and only with free disk space of the database's size plus 10 %. It takes about as long as a backup; reads continue, and a write that waits longer than 5 seconds fails as busy and is retried by the device. Without the space the server logs a warning, keeps removing old note versions and the file stops growing. New databases need nothing. No migration, no new variable. (Database size and compaction)

Added

  • Your own accent colour, with contrast guaranteed. Eight themes were the only choice, so a freelancer's brand colour or a team's colour could not be the app's accent. A ninth tile, Custom, in Settings → Appearance → Theme opens a picker (a bottom sheet on a phone): 18 swatches as a radio group (arrow keys move, Enter or Space picks) and a hex field (#1f6feb, 1f6feb or #f60; Enter or leaving the field applies it, anything else shows an error), with a preview of a button, a link, a selected row and a soft chip in light and dark while you choose. The device derives the light accent, the bright dark-mode accent and, when needed, a lighter dark-mode button from the one colour, keeping its hue and moving only its lightness until every accent contrast pair of the design system passes in both modes (a pale yellow becomes a darker yellow in light and a light yellow in dark); a colour close to the red of errors and priority 1 is turned away from it until it is as far as Rose is. When the colour was adjusted the picker says so and shows the picked and the used colour. Choosing another theme keeps the colour for next time; the browser and system bar uses the derived light accent; the first frame is already in your colour (the derived values are cached on the device, the derivation itself loads lazily). Synced as the additive user_prefs.theme_accent_custom beside theme_accent: "custom" and carried by the export and import; older apps show Emerald and write nothing. High contrast still shows black or white. (Your own accent colour, Design system, accent-colour, closes #563)
  • Fold a board column, and show notes on cards. A finished or parked column ("Done", "Later") took as much width as the one being worked on, and the line of context in a card's notes was a click away. Choose Fold column in a column's menu, or press z on one of its cards, and the column becomes a narrow strip with its colour or icon, its open-task count and its name written sideways; select the strip (or Enter / z on it) to unfold it. A folded column still takes cards: drop one on the strip or press Shift+→ / Shift+← into it, the card goes to the bottom, the count goes up, the move is announced and the column stays folded; h / l skip it, and on a phone it is a 48 px strip between the others with a dot per unfolded column. Folding is kept on this device only and is the same as collapsing that section in the list layout; saved views in the board layout fold their columns the same way, and a board of filters folds a panel to its header line. Show notes on cards in a project board's menu (your own choice for that project, synced to your devices; Notes on cards in a board view's display options) adds the first two lines of each card's notes as plain text under its title, read from the notes on the device, so it works offline and on encrypted accounts and never shows ciphertext (#561).
  • Several months in the Month, and a Year view. The calendar stopped at one month, so a term, a quarter of launches or a training block could not be seen at once. Choose Months shown: 2, 3 or 6 in the Month's page menu to stack that many months, each with its name and only its own days, so a day and its tasks appear once; the arrows and j / k run on across them, [ and ] still move by one month, and this device remembers the choice (a phone always shows one month). Year (g y, and in the Day | Week | Month | Plan | Year switch) shows the twelve months as small grids with a mark under each day in four steps (nothing, 1–2, 3–5, 6 or more) that also differ in length and in the weight of the number, a red dot where the estimates exceed your day length, and a tip with the day's count and first three titles. It counts open tasks, every day of a multi-day task and, while Show repeating occurrences is on, the coming occurrences of repeating tasks; events and deadlines don't count, and the calendar's filter and a project's Calendar layout apply. Click a day for Day and a month's name for Month; with the keyboard the twelve months are one grid (arrows across month edges, Page Up / Page Down a month, Home / End, [ / ] a year, Enter opens the day). On a phone a month is one touch target that opens it in Month. Month and Year now load on first use, which took 1 KB off the first load. Everything is read from the device and works offline (#557).
  • Tasks that span days. A trip, a conference or a week of writing showed as one dot on its last day, so the calendar never said that Tuesday to Thursday were taken. Turn on Multi-day in the When editor's Start tab (it sets the start to the day before when there is none) and the task is drawn from its start date to its do-on date: one bar in the Week's all-day strip in the project's colour, the day's piece in Day with "Day 2 of 3" and a due time as "until 17:00", a bar across each of Month's week rows that continues on the next, and a row in each Plan column with its position; on a phone the Week lists it under each day it covers and the Month counts it on each. Drag the bar to move start and due together, drag an end to change the first or last day, or drag the right end of a task without a time onto a later day to make it a span; with a span selected, ←/→ move it, Shift+←/Shift+→ move the last day and Alt+Shift+←/Alt+Shift+→ the first, each one change with Undo (Shift+→ on a task without a time makes it a two-day span). Upcoming lists a span once under its first day with its range ("3–5 Nov"), Today shows it during the span with "until 5 Nov", and every list row shows the range in the date's place. Its estimate counts on the last day only; a repeating span keeps its length, and its coming occurrences are drawn as dimmed bars. The flag is the additive boolean task.span: start dates keep meaning "defer until" for every other task, and a flag without a valid range draws as a plain task. REST and the export carry it, ot add/ot edit take --span/--no-span and ot list prints the range, AI tools read and write it, CalDAV serves X-OPENTODO-SPAN:TRUE (other CalDAV apps show the task on its last day), the calendar feed and calendar sync write a span as one all-day event and read a moved or resized event back into start and due, and the Markdown vault writes [span:: true]. (Tasks that span days, API, CalDAV, multi-day-tasks, closes #558)
  • A Notes page, standalone notes and note search. Notes in the sidebar's More group (g e, promotable to the main destinations) lists your task notes, project notes and a new kind of note that is not a task, newest change first, 50 at a time with Show more, filtered by source, project and label. Write meeting notes or a reference page with New note (q on the page), in a project and with labels; a note in a shared project is shared with it (editors write, viewers read). The search field finds every word you type in titles and text, ignoring case, accents and formatting, with the matching words highlighted, on your device only, so it works offline and with end-to-end encryption; the command palette (Ctrl+K) finds notes too, and the filter language gains notes, no notes and notes: text. Turn into task and Move note to a new note convert in both directions with one Undo, and notes go to the Trash like tasks. Scripts and assistants get ot notes and ot task note, the REST collection /api/v1/notes with POST …/append and …/to-task, and MCP tools to list, search, read, create, update, append to, trash and convert notes behind the write gate. Text added from outside is appended at the end without replacing what you wrote (the additive appends.<id> field), and replacing a note's text checks that nobody changed it since it was read (body_version, If-Match, 409 body_changed). Notes are not sent over CalDAV, webhooks, the activity feed, automations or the Markdown vault. (Notes, Notes, tasks and scripts, API, notes-overview, closes #655)
  • A Formatting button beside notes on a computer. The formatting row (bold, italic, link, bullet list, checklist and More) was shown only on touch screens; on a computer, a task's or a project's note now has a small Formatting button at its top corner, shown when you point at the note or move into it, that keeps the row above the note until you press it again. Whether the row is shown is remembered on each computer, separately for notes and for the comment box, whose Formatting button now remembers its row too. The row is a proper toolbar: one Tab stop, the arrow keys, Home and End move between its buttons, Down opens More, Escape returns to the text with its selection, and a button puts you back in the text. Formatting button in Settings → Account → Profile → How you work (on by default, synced as the additive user_prefs.formatting_toggle) hides the button in notes and comments; shortcuts, typed formatting and the / menu keep working, and phones are unchanged. (The formatting row, notes-toolbar-desktop, closes #654)
  • The server removes old note versions. Every save of a task's or a project's note stored the whole note, so the database kept every old copy and a new device downloaded them all. Once a day, after something changed, a background job deletes the superseded note documents the server received more than 30 days ago, in every scope, shared projects included, and keeps the last readable revision of each UTC day for older edits; the current note and every other field are never touched, and devices converge exactly as before (cursors stay valid, offline devices keep their edits). It works in short steps, so syncing is never held up, and returns the freed space to the file system. Settings → Server → Backups → Database storage shows the owner the database's size, the free space inside it and what the last compaction removed and reclaimed (GET /api/v1/settings/storage), and the History of a task or project says in one line that older note versions are kept one per day. On a host the job runs per tenant and lowers its operation count. (Sync, Backups, notes-compaction, closes #652)
  • Fix a repeating task's past occurrences too: "All occurrences" asks "Only future ones" or "Also past ones". All occurrences changed a series from its current occurrence on, so renaming "Spansh class" left every completed session, and the series before a change from a date on, with the typo. Now, when a series has a past (a done or skipped occurrence, or an earlier part from "This and following"), a second choice appears under the scope control in the task panel: Only future ones (the default, as before) or Also past ones. With Also past ones, a new title, notes, labels, project or priority also reaches the past occurrences that still held the old value and the earlier parts of the series, in one step with one Undo; past dates and completions never change, and a new time or rule only reaches the earlier parts' own dates. Works offline; on an encrypted account the device encrypts each occurrence itself. The API's PATCH of a series still changes that task only. See Recurring task rules.
  • Change a repeating task from one occurrence onward; history, streaks and counts carry across. Changing a routine from a date on (the Spanish class at 19:00 after the break, gym days from Monday to Tuesday) meant editing the whole series or ending it and starting again, which lost its history and a habit's streak. Open a coming occurrence and choose This and following between This occurrence and All occurrences: the first change there ends the series the day before (its rule gets until) and starts a new series on that date with the change, in one batch with one Undo ("Changed from Wednesday 14 October on"). Done, skipped and moved occurrences after the date stay as they are; open ones that still hold the old value take the change. The new series carries the additive task.split_from naming the series and date and a derived id, so two devices making the same split offline make one, and splits at two dates on two devices converge: each piece runs to the day before the next one, and any device writes that until into the rule. for N times counts over all pieces, so twenty sessions stay twenty; a habit's streak and month grid read every piece; the panel reads "Continues Study from Oct 14" and "Continues as Study from Oct 14". Delete this and following in a projection's menu ends the series the day before. Scripts use POST /api/v1/tasks/{id}/split and ot task split <task> --from <date>, and the occurrence list continues into the pieces with each date's series_id; CalDAV serves each piece as its own repeating VTODO with RELATED-TO;RELTYPE=SIBLING and the ended series with its UNTIL; the vault writes each as its own line. (Recurring task rules, API, CalDAV, series-split, closes #552; thanks to @pieros7 for the request in #537)
  • Act on one occurrence of a repeating task: complete it early, skip it, move or edit it alone; CalDAV exceptions. Only the current occurrence could be detached, and a skip always moved the whole series. Now any coming occurrence can be changed alone, as calendars do with "this event only": right-click, long-press or press Shift+F10 on a projected occurrence for Complete this occurrence, Skip this occurrence (Skip this day for a habit) or Move to…, or drag it with the mouse to another day or time in Day, Week, Month and Plan, with Undo. Opening one shows the panel on This occurrence · All occurrences, with the date, the task's values, its actions and Edit this occurrence; "This occurrence only" (Shift+O) now writes the same occurrence record. A changed occurrence is a task of its own with the additive task.occurrence_date naming the date it stands for; that date becomes an exception of the series, so no projection is drawn there and completing the series jumps over it, and two devices acting on different dates offline keep both (a device that rolled before it saw another's record moves the series on after the next sync). for N times counts done and open occurrences, never skipped ones; a moved occurrence gets copies of the series' relative reminders; renaming the series renames the moved ones that still had the old title; deleting an open occurrence skips it; a habit's grid counts a moved day on the day it stands for. CalDAV serves exceptions as EXDATE, moved occurrences as their own VTODOs and counted rules with UNTIL, and a client's EXDATE and RECURRENCE-ID overrides of any date (completed, cancelled or changed) become the matching occurrence; the Markdown vault jumps over exceptions too. Scripts use POST /api/v1/tasks/{id}/occurrences/{date}/complete and /skip, PATCH /api/v1/tasks/{id}/occurrences/{date} and ot task occurrence <task> <date> complete|skip|edit; the occurrence list reports open, done and skipped. On an encrypted account server-made records carry no title until an unlocked device fills them in. "This and following" comes with series-split. (Repeating tasks on the calendar, Recurring task rules, API, CalDAV, series-exceptions, closes #551)
  • Future occurrences of repeating tasks and habits in Day, Week and Month, and optionally in Upcoming. A repeating task stood on one date only, so next Tuesday's study session or Friday's habit was missing until the earlier one was completed. Day, Week and Month now draw the occurrences that follow the current one as faint blocks and rows with a dashed edge and the repeat icon, computed on the device from the rule (nothing is written or synced, offline included), stopped by until and for N times, and counted in each day's planned total ("45m repeating"). They can't be dragged, completed or selected with j/k; Tab reaches them and a click or Enter opens the task with the date named under its title. The calendar's filter, Colour by and a project's Calendar layout apply to them. Show repeating occurrences in the calendar's menu turns them off on a device (on by default; the Plan keeps them), and the same item in Upcoming's menu (off by default) lists the next 14 days' occurrences under their days. Scripts and assistants read the same dates with GET /api/v1/tasks/{id}/occurrences, ot task occurrences and the next_occurrences of a recurring task in the AI tools; the calculation is shared by the server and the app and pinned by web/src/lib/occurrences.vectors.json. Acting on one occurrence alone and "this and following" come with series-exceptions and series-split. (Repeating tasks on the calendar, Recurring task rules, API, series-occurrences, closes #550)
  • The OpenTodo account on X, from Settings → About and Help. A News and updates row in Settings → About opens OpenTodo on X, where each release and the work in progress are posted in a few lines; the settings search finds it under "twitter" and "news". A plain link in a new tab, like the Discord row: nothing in the app talks to X. The README and the Feedback and support guide name it too. (Feedback and support, x-account-link)

Changed

  • The Inbox shows only the Inbox. The folded Planned, no project group at the end of the Inbox is gone, with its Open as a new view action: dated tasks seemed to be in the Inbox while it said Inbox zero, and tasks without a project are too common to need a section. An empty Inbox shows its full Inbox zero picture again. Planned tasks without a project are where their dates put them, in Today and Upcoming, and Filter with no project lists them all. Quick add in the Inbox still says where a dated task went ("Added to Friday"). (Today and Upcoming, inbox-only-inbox)
  • Notes are sent after a pause, not after every keystroke pause. The note editor still saves on the device 400 ms after you stop typing, so a crash or a closed tab loses nothing, but it now sends the note to the server only 3 seconds after its last save, at once when you leave the note (it loses focus, the task or project closes, another task opens, the app navigates away, the page is hidden or closed), and at least every 60 seconds while you keep typing. Before, every short pause sent a full copy of the note, up to 256 KB each. Any other sync carries the waiting note too, the device keeps only the newest copy and its text, collaborators see your text about 3 seconds after you pause, offline writing and convergence are unchanged, and comments are not affected. (Sync, Project notes, notes-save-cadence, closes #653)

Fixed

  • An attachment uploaded right after another one is no longer refused as over the quota. An upload held its size against the attachment quota until a moment after its answer was sent, so a device that sent the next file at once could find the finished file counted twice and get "this upload would go over your quota" with room to spare. The room is now given back before the answer is sent; parallel uploads still share the free space.
  • "Added to the bottom of …" shows on a slow device too. After a quick add, the app waited only twelve frames for the new row before deciding whether it was out of sight, which a busy phone or computer could miss, so the toast with Show never came. It now waits for the row itself, for up to 3 seconds.
  • Offline, a part of the app that was not loaded yet no longer reloads the page. When a piece of the app loaded on first use (such as the "Added to the bottom of …" announcement after a quick add) could not be fetched because the device was offline, the app took it for a newer release and reloaded, which showed an empty page until the connection came back if the app was not installed yet. Offline, the app now stays on the page and does without that piece; online, a missing piece still reloads into the new release.

0.18.1

· On GitHub

OpenTodo 0.18.1 is a fix release. A device whose clock runs fast no longer loses an edit when it has more than 500 changes waiting to upload, and the browser extension 0.6.2 shortens its store description so the Chrome Web Store accepts it. Nothing to do when upgrading.

All changes 2

Fixed

  • A device whose clock ran fast no longer loses its later edit when it has many changes to upload. When the server refused a fast clock's changes in more than one batch of an exchange, the device set its clock back to the server's time for each batch, so a change in a later batch could be stamped before an earlier change of the same field: the server kept the earlier value while the device showed the later one. The clock now restarts only when it is more than 2.5 minutes ahead of the server, so the changes keep their order. (Sync, closes #651)
  • The browser extension 0.6.2 can be published in the Chrome Web Store. The store refused 0.6.1 because its description was longer than 132 characters in every language. Each language now has a shorter description with the same meaning, and a test keeps the name and description within the store's limits. (Capture)

0.18.0

· On GitHub

OpenTodo 0.18.0 is about planning the day on the calendar. Meetings from your subscribed and connected calendars can count toward the day's capacity, Re-plan moves the blocks a slipped morning missed into the free time left, frames mark the hours you keep for one kind of work and slots gather several small tasks in one block. The calendar shows deadlines on their day, adds a task with a double-click, colours blocks by project, label or priority, takes a filter and can show completed tasks; a project gains a Calendar layout. Tasks can carry an energy (Deep, Medium, Low) to filter by, and the Daily check offers Bento: one Large, one Medium and one Small task. Label pages and saved views keep their own manual order, quick add reads every other friday and stops taking a month named in passing as the date, today's focus and the Bento slots can be filled by touch, a tap beside a task's circle completes it on a phone, and calendars published from Exchange on-premises load. Every new setting is off by default.

All changes 17

Upgrade notes

  • Replace the image or the binary and restart. One migration (0050_calendar_feed_browser_agent: a table that remembers which calendar feeds need a browser User-Agent); no new variables. Frames and slots are new synced entities and energy and manual_order new synced fields, so upgrade the server before the devices, as for every release. Back up first; there are no down-migrations.

Added

  • Calendar clarity: deadlines on the calendar, adding with the mouse, Colour by and a filter. A task's deadline now shows on its day: a chip with an amber diamond in the Day and Week strip under the day's name (before all-day events, within the same three-item limit and +N), a row at the end of a Month day (counted in +N more) and of a Plan column, or the diamond on the task itself when it is planned on its deadline day; markers open the task with a click or Enter, are dimmed on past days, never count toward capacity and can't be dragged. Double-click an empty quarter hour of Day or Week, a day's Unscheduled lane or an empty part of a Month day to add a task there (created at once, offline too); a right-click or a 500 ms long press opens Add task here…, New frame here… and New slot here…; Month days and the quarter hour under a mouse are highlighted; q in Day and Week adds on the focused day, and at 4pm typed there sets the time on that day (#555). Colour by Project, Label or Priority in the calendar's menu (Day, Week, Month, Plan) and for boards (a project board's menu, a board view's Display options; a 3 px edge on each card), a device choice that is never synced (#554). Filter… narrows the calendar's four modes, or Upcoming, with a query in the filter language and label checkboxes that write @errand | @call; the active filter is one chip in the header, capacity still counts every task and its tooltip says how many are hidden, an invalid edit keeps the last valid filter, and each filter is remembered per device (#556). A project's layout switch gains Calendar after Timeline (v cycles List, Board, Timeline, Calendar): the calendar in its last mode, narrowed to the project and its sub-projects, inside the project page (#556). No op field, route or variable; new device meta keys only. (Add and colour on the calendar, Filter the calendar, calendar-clarity, closes #554, #555 and #556)
  • Energy: Deep, Medium or Low on a task. Turn on Energy levels in Settings → Account → How you work (off by default in every mode) and give a task the energy it asks for: from Add on the task page and the docked panel (a Deep / Medium / Low choice that works by touch and with the arrow keys), the row menu's Energy submenu (with Undo), or ~deep, ~medium, ~low (or ~d, ~m, ~l) in quick add, in every language. A row shows the word when set, as a property a saved view can hide. The filter language gains energy, no energy and energy: low, so energy: low & unblocked & today is an end-of-the-day list of easy wins. Turning the switch off hides the choice, never a value. The field travels everywhere a task does: the REST task shape (invalid_energy for anything else), ot add/edit --energy and an ENERGY list column, the agent tools and MCP, CSV import (an energy column), the export, CalDAV (X-OPENTODO-ENERGY) and the Markdown vault ([energy:: low]). It is not content, so it stays plaintext on an encrypted account like the priority. (task-energy)
  • See completed tasks on the calendar. Choose Show completed tasks in Day, Week, Month or Plan to show finished tasks at their scheduled date and time with struck-through titles. The choice is off by default, remembered on this device and works offline. Finished tasks can be opened for inspection and do not add to remaining planned capacity. Thanks to @arvkonstantin for the idea and the contribution (#626). (Calendar views, calendar-completed-tasks, closes #624)
  • Meetings can count toward the day's capacity. Settings → Language and region → Calendar grid → Count meetings in capacity (shown once this device knows a subscribed or connected calendar, off by default) adds the busy meetings Day, Week and Month draw to each day's planned total: 6h 45m / 8h, with "4h 15m of tasks, 2h 30m in meetings" in the header's tooltip. Meetings are counted as the Plan counts them: timed busy events of enabled calendars, overlaps once, split at midnight; free and all-day events never, and nothing while the toolbar's Calendars button hides events. A day whose tasks alone go over stays red (Over capacity); one whose tasks fit but not around its meetings turns amber (Over with meetings), and both are said to screen readers. Offline, a day outside the device's event cache counts no meetings and its tooltip says so. The setting belongs to each device, like the day length; no op field, no server change. The capacity tooltip of the Day and Week headers, which never opened, now opens on hover and focus. (Place tasks on the calendar, meeting-aware-capacity)
  • Re-plan the rest of the day. When the morning slips, the Day and Week header reads 3 blocks behind · Re-plan (also in the calendar's and Today's menus and the command palette, only while something is behind). Re-plan moves today's missed time blocks, open timed tasks whose block has ended, in their original order and at their own length, into the first free time between the next quarter hour and the end of your grid, around other timed tasks and busy events of your calendars. It is a preview first: dashed ghosts at the new places and the old blocks faded, "4 tasks move · 1 doesn't fit", a choice to Leave the tasks that don't fit or Move them to tomorrow (with After its deadline where it applies), Apply (Enter) and Cancel (Esc); on a phone, a sheet listing "09:00 → 14:30 Write report". The preview follows completions and moves from other devices. Apply writes one batch of ordinary due changes with one Undo; tasks you may only view, tasks assigned to someone else, the task your timer runs on and tasks changed meanwhile are never moved. App only: no op field, no server, CLI or MCP change (assistants can still use schedule_tasks). (Re-plan the rest of the day, replan-the-day)
  • Frames: the hours you keep for one kind of work. A frame such as Deep work on weekdays from 09:00 to 11:00, or Admin Monday to Thursday late in the afternoon, shows as a quiet tinted band behind the Day and Week blocks, with its title and times at the top; two frames that overlap share the column, and past days are dimmed. Frames only guide: they never move, block or create anything, tasks dropped inside one land where you drop them, and capacity and Re-plan ignore them. Manage them from ⋯ › Frames… in Day and Week or New frame in the command palette: title, days (in your week's order), start and end in quarter hours, colour, an optional Plan from project, label or saved view, and optional first and last days, up to 50 frames. A frame's title opens Plan from this frame (the planning list on the frame's source; s then proposes the first free quarter hour inside the frame that holds the task), Edit and Delete with Undo. On a phone the Week's list shows each frame as a heading above the tasks inside it. Frames are personal and sync to your own devices only; the title is encrypted with end-to-end encryption. REST /api/v1/frames, ot frames (add, rm), the read tool list_frames and the resource opentodo://frames read them; the export includes them. New synced entity frame, no migration. (Frames, calendar-frames)
  • Slots: several small tasks in one calendar block. A slot such as Admin · 16:00–16:45 is one block on the Day and Week grid with a stack icon, its title, a count (2 of 5 done) and a checklist of the small tasks done in it, as many rows as fit and +n for the rest; the tasks are no longer drawn as slivers or in the Unscheduled lane. Create one with ⋯ › New slot (the next free quarter hour), New slot in the command palette or Make a slot here in a frame's details. Drop a task on it, press Shift+Enter during a placement (s) over it, or pick Add to Admin 16:00 in a phone's Schedule sheet: the task is dated to the slot's day without a time in the same batch, with one Undo; a slot holds up to 20 tasks, and a task you may only view is added only when already on that day. Rows complete with a tick or Space, open with Enter, and can be dragged out onto the grid; the slot moves (its tasks follow to a new day) and resizes like a block, and Delete slot leaves its tasks in the lane. A slot counts once toward the day's capacity in Day, Week, Month and Plan, its length or its tasks' estimates when they add up to more (50m in 45m in the warning tone); Re-plan treats it as busy time. In lists a slotted task shows the slot's start with the stack icon in the time column; on a phone the Week lists a slot as one row that expands to its tasks. Slots are personal and sync to your own devices only; the title is encrypted with end-to-end encryption. REST /api/v1/slots (with move_tasks), ot slots (add, put, rm), the read tool list_slots and the resource opentodo://slots read them; the export includes them. New synced entity slot, no migration. (Slots, calendar-slots)
  • Bento in the Daily check: one Large, one Medium and one Small task. Choose Daily focus › Bento in the Daily check's ⋯ menu or in Settings → Account → How you work, and today's focus becomes three slots above the list. f puts a task in the slot its estimate suggests (Small up to 30 minutes, Medium 31 to 89, Large 90 or more); when that slot is taken the message names the task in it and offers Choose a slot, and a task without an estimate opens a menu (a bottom sheet on a phone) with the free slots and Set estimate… (only for tasks you may edit). Any task can go in any free slot, a slot never changes the task, and every change has Undo. The Order, Frog (Large first, the default) or Summit (Medium first), applies to the Daily check, Today's focus on the Loop home and the task the Focus screen opens on. Three tasks stays the default and is unchanged. The slots are a new additive field review.focus_slots beside focus, which older devices keep reading; the choices are user_prefs.focus_style and bento_order. No migration, no API change. (Rhythms, bento-daily-check)

Changed

  • Quick add understands "every other". Send timesheet every other friday repeats every 2 weeks from the next Friday, and every other day, week, month and year repeat every 2 of them, with the phrase removed from the title. In the other languages: jeden zweiten Freitag, jede zweite Woche, alle zwei Tage; un vendredi sur deux, tous les deux jours; ogni due venerdì, a settimane alterne; cada dos viernes; a cada duas sextas. German jeden zweiten Freitag without im Monat was the second Friday of each month and is now every other Friday. The CLI and the parse route read the same. (Quick add, quick-add-month-and-every-other, closes #635)
  • The quick-add parse route follows your Day parts preference. POST /api/v1/quick-add/parse without day_parts now reads the part phrases (tomorrow morning, this afternoon) when the account's Group by day part is on; an explicit day_parts still wins either way. The browser extension's Quick save, which never sends the field, now previews and saves part phrases for accounts with day parts on. (API, Day parts, quick-add-day-parts-default)

Fixed

  • A month named in passing no longer becomes the due date. In Collect the invoices from September and send them to the accountant next monday at 10am, quick add took September as the date and cut it from the title. A month name alone now gives way to a later date in the line and stays in the title, and right after from, since, of or for (and the same words in the other five languages) it is never a date. Pay rent in September is still due on 1 September. The CLI and the parse route read the same. (Quick add, quick-add-month-and-every-other, closes #634)
  • Calendars published from Exchange on-premises load. Outlook on the web on an organisation's own Exchange server answers 400 to any client that isn't a desktop browser, so its .ics addresses failed with http_400. Calendar feeds still identify as OpenTodo first; when a calendar answers 400, 403 or 406, the fetch is tried once more with a desktop browser's User-Agent, and a calendar that only answers that way keeps getting it until its address changes. No other error is retried. Thanks to @osenchenko for the diagnosis. New server-side table (migration 0050), no op field or API change. (Calendar feeds, calendar-feed-user-agent, closes #622)
  • List rows on wide touch screens line up again. On a touch screen 768 px or wider (a tablet in landscape, a touch laptop), a list row's title and its property column sat 10 px below the completion circle, because the row's 44 px touch height centred them; they now stay on the circle's line.
  • A tap next to a task's circle completes it on a phone. The completion circle stays 20 px, and its 44 × 44 px touch area now sits above the rest of the row, so a tap a few pixels off the circle no longer opens the task, also while the row is moving (the first swipe hint, a swipe returning). Every row with a circle: lists, Today, board cards, the Daily check and the Loop home. The layout check now taps 8 px around the circle on every screen. (phone-touch-focus, closes #636)
  • Today's focus and the Bento slots can be filled by touch. They only answered f. Tap an empty Bento slot in the Daily check to choose a task for it (today's and overdue tasks first, those whose estimate fits the slot first), or Add to focus under Three tasks in the Daily check and on the Loop home. A task's menu (the Actions menu, which a phone opens by holding the task, the task panel and page, and the Focus screen) offers Add to focus or Put in a slot, and Remove from focus, following the same rules as f; a task of a shared project you only view can join your focus too. Every change has Undo, now also f in Three tasks. (Pick the focus by touch, phone-touch-focus, closes #641)
  • Reorder tasks on label pages and in saved views (#623, reported by @osenchenko). Order by: Manual order was offered there but dragging or Alt+↑ / ↓ did nothing. A label's page and a saved list view now keep their own manual order: drag a task above or below another one in its group (press and hold on a phone), or move it with Alt+↑ / ↓, Alt+Shift+↑ / ↓ or Vim's Shift+J / K, with Order changed and Undo. Under another order a move switches the page to Manual order first. The order is stored on the label or view (new synced field manual_order, at most 500 task ids), follows you to your devices, works offline and never reorders a project, so a viewer of a shared project can order their own label page or view. Tasks never moved follow in their project order. The unsaved New view page no longer offers Manual order; save the view first.

0.17.0

· On GitHub

OpenTodo 0.17.0 adds calm limits to a busy day, each off by default: an optional Today limit with Done today and Log a win, Morning, Afternoon and This Evening in Today, sequential projects and areas, and a review interval per project with a Maintenance status for projects on low burn. Focus is one tap away from every task, new tasks can get a default reminder and can be added at the top, a pasted list becomes one task per line, the sidebar shows counts on every list in your own order, and a right-click on a task opens its menu. Sub-projects inherit their parent's custom fields, and a round of fixes covers Gemini and compatible assistant providers, CalDAV events from iOS Calendar, the selection bar, short calendar items and copying over plain http.

All changes 30

Upgrade notes

  • Replace the image or the binary and restart. No migrations and no new variables; the new settings are additive fields with defaults that keep today's behaviour. Upgrade the server before the devices, as for every release.
  • The old image name is no longer updated. From 0.17.0 releases are published only as ghcr.io/opentodoapp/opentodo (and the chart as oci://ghcr.io/opentodoapp/charts/opentodo); ghcr.io/pixari/opentodo stays at 0.16.0. If your compose file, docker run command or template still names it, switch now. (Docker)

Added

  • An optional Today limit, Done today, and Log a win. Settings → General → Start view and briefing → Today limit (or Limit Today… in Today's menu in the method modes) caps how many tasks Today lists: 3, 5, 7, 10 or any number from 1 to 20. The slots fill first come, first served (today's focus, the tasks you let in, Earlier oldest first, then the rest by when they were created), so a task captured on a full day waits instead of pushing out a planned one; the others sit in a folded Waiting for a slot group, and the line under the title reads 3 of 5. Take a slot (on the row, in the task's menu or the palette) lets a waiting task in for the day. Blocked tasks, later steps of a sequential project and habits never take a slot, and nothing is ever moved or refused. A folded Done today group lists what you completed today (shown by default in the method modes), and Log a win adds work that was never a task, already done. today_summary reports today_limit and waiting_for_slot, ot today prints a "Waiting for a slot" section (--all for one list) and ot add --done logs a win from the terminal. Off by default; Simple lists users see no change (Today limit and Done today).
  • Morning, Afternoon and This Evening in Today, for tasks and habits. A task or a habit can now belong to a part of the day: Morning or Afternoon, beside the existing This Evening. Turn on Group by day part (Settings → General → Start view and briefing, or Today's menu in the method modes) and Today lists the day under Today, Morning, Afternoon and This Evening; a task with a time but no part goes to the part of its time (before 12:00, until 17:59, from 18:00), and a part you chose always wins. Set a part in the When editor (Any time, Morning, Afternoon, Evening), by dragging a task onto a part's heading, from the Actions menu's Day part or the command palette, each with Undo; Shift+E still toggles This Evening. Quick add reads this morning, this afternoon and a part after a date (tomorrow morning, fri evening) in all six languages while day parts are on, and Morning run tomorrow keeps its title. Filters gain morning and afternoon; the part reaches CalDAV (X-OPENTODO-DAY-PART), the Markdown vault ([daypart:: morning]), ot add/edit --part, the REST API and the assistant tools. Off by default: with day parts off Today is unchanged, and a part set elsewhere shows as a chip. (Day parts)
  • A review interval per project, and Maintenance for projects on low burn. The weekly review's project walk now shows only the projects that are due: each project comes up every week unless you choose Review every… in its menu (or press e on its card) for every 2 weeks, every month, every quarter or paused, counted in your own days (reviewed on 31 January, monthly, due on 28 February). The interval is yours: in a shared project each member keeps their own rhythm. After the last card the walk says how many projects are not due yet and how many are paused, and Review them too adds them. Put in maintenance (project menu, the row menu on All projects, or m in the walk) marks a project as deliberately on low burn for everyone who shares it: its health reads Maintenance instead of Overdue, Quiet or On track (a passed deadline still shows Late), it is never stalled or without a next action in the review, on the Loop home or in the weekly email, and its tasks stay where they were. Under Simple lists both items appear only once something other than the default is set. REST (status on projects, active or maintenance), the agent tools and MCP (update_project status, status in project output), the STATUS column of ot project list and opentodo-status in vault files follow. Additive fields project.status and project_prefs.review_every; no migration. (Review intervals and maintenance, project-review-cadence, closes #575)
  • Sequential projects and sections, and areas that never finish. Flow… in a project's menu makes it Sequential: only the first open step is available, and the steps after it stay in place, dimmed, with After and the step they wait for, until it is completed (offline too, with nothing extra written). Sections are steps of their own and can be Parallel or Sequential from their heading or column menu; a Sequential section in an ordinary project offers its tasks one at a time. A step that starts later, is blocked or is assigned to someone else holds the sequence; recurring tasks, habits and Someday never do. Area marks an ongoing list such as Home or Finances: no progress on All projects (it reads Area), never Quiet and never "without a next action" in the review, the Loop home or the weekly email. Today folds due steps that wait into its Blocked group, Next shows one step per sequential project, and the new filter keyword available lists what can be done now. Under Simple lists the item only appears where a flow is set; the palette has Set project flow…. REST (flow_mode on projects and sections), the agent tools and MCP (flow_mode, list_tasks available), ot list --available, a FLOW column in ot project list and opentodo-flow in vault files follow, all computed by one shared rule (internal/flow and web/src/lib/flow.ts, pinned by shared vectors). Additive fields project.flow_mode and section.flow_mode; no migration. (Sequential projects and areas, project-flow-modes, closes #574)
  • Focus is easy to find, and its timer got three comforts. Focus is now in the sidebar's More group after Statistics (and can be shown beside Inbox and Today from Settings → Appearance), a clock button at the top of the task panel starts it in one tap, and every task menu (., the row's ⋯, a right-click, or on a phone Actions with one task ticked after a long press) has Start focus: it opens the Focus screen with a session already running (Countdown asks for the length first), and switching from another task's session names it in a notice with Undo. When a Pomodoro or Countdown session ends, +5 min and +15 min carry on with the same task for up to 10 minutes after the end. The Countdown dialog shows your presets (10, 20 and 40 minutes unless you change them) as one-tap buttons, and Background noise (white, pink or brown, generated on the device, off by default) plays only while you work, with the volume set per device. A long press on a phone now vibrates only while haptic feedback is on. Two additive user_prefs fields, focus_presets and focus_noise; no migration. (Focus, Focus breaks, presets and noise, focus-everywhere, closes #543, #553 and #535)
  • A calmer sidebar you can tune: counts on every list, your own order, icons for views, and a New view draft that waits. Settings → Appearance → Sidebar → Sidebar counts adds a choice, Every list, that shows the number of open tasks on every project, label and saved view (and their favourites) at rest; with a mouse, pointing at a row puts its ⋯ button where the number was. The default stays Inbox and Today. Sidebar destinations lists Inbox, Today, Upcoming, Calendar and the More destinations in one list you reorder by dragging, from each row's menu or with Alt+↑ / ↓; the order syncs, the phone's tab bar keeps Today, Inbox and Upcoming, and Open Loops first keeps its six places. A saved view's menu has Icon…, the projects' icon picker; the icon shows in the sidebar, Favourites, the views list, the search box and the view's header, and /api/v1/views accepts icon (a catalogue name or null). The New view page keeps its query and display options on the device, per workspace, across navigation and reloads, until you save the view or choose Clear (with Undo). Additive fields user_prefs.sidebar_counts, user_prefs.sidebar_order and view.icon; no migration. (Sidebar counts and order, Saved views, sidebar-polish, closes #545, #562, #560 and #559)
  • Paste a list, get one task per line. Pasting two or more lines into an empty quick add (a list's, a section's, the command bar's, the phone's add sheet, the share screen or a calendar day's) no longer squashes them into one long title: OpenTodo asks Add 7 tasks, one per line? with a preview, and Add 7 tasks (or Enter) creates them in the pasted order, while Paste as one line keeps the old behaviour and Esc pastes nothing. List markers (-, *, •, 1., a), [ ], - [x], ☐ …) and blank lines are dropped, and with parsing on each line is read like a typed one, with the view's defaults for the rest. All tasks of a paste are one batch, offline too, and one Undo removes them all; a line too long to encrypt refuses the whole paste. The Add subtask line does the same with Add N subtasks, without parsing. Up to 100 lines at a time; longer lists point to Import. A paste into a box that already holds text is untouched. No server change. (Quick add syntax, paste-to-tasks, closes #578)
  • Default reminders for tasks with a date or a time. Settings → Notifications → Default reminders sets one reminder for tasks with a time (at the due time, 10 or 30 minutes, 1 hour or 1 day before, or a custom offset) and one for tasks with a date only (on the day at 09:00, 1, 2 or 3 days or a week before, or custom). When a task without a date gets one, by quick add, the date picker, the calendar, bulk Set date, an AI assistant, ot add/ot edit or the browser extension, it gets that reminder: an ordinary reminder you can change or delete, delivered by push, the phone apps and CalDAV like any other. It is added once per task (a deleted one stays deleted, Undo takes it back with the date), your own reminder from quick add wins, it follows a time being added or removed, and habits, teammates' scheduling, CalDAV, calendar sync, imports, rules and email never add one. Both start at None. REST callers opt in with ?default_reminder=true on POST /api/v1/tasks and PATCH /api/v1/tasks/{id}; ot has --no-default-reminder. Two additive user_prefs fields, reminder_default_timed and reminder_default_dated; no migration. (Default reminders, default-reminders, closes #546)
  • Right-click a task row to get its menu, where you clicked. Until now a right-click only offered the browser's menu, which knows nothing about a task: pointer users had to tick the row to raise the Selection bar or reach for .. Right-clicking a row in a list, board, filter, saved view or method list now opens the same Actions menu the keyboard opens, at the pointer, for the whole ticked selection when that row is ticked and for the row alone when it is not — and a touch hold opens it at the finger, on a phone as on a laptop. Links, fields and selected text inside a row keep the browser's own menu; a long press no longer ticks rows (the row's new grip does that instead). (Work from the list, Keyboard, task-row-context-menu)
  • Select on a phone from a grip at the row's edge. Each row now shows a small marker at its right end on touch screens: a tap ticks the row (or unticks it), a hold does the same without moving, and moving it drags the ticked selection — the grip never scrolls, so the drag starts at once. This replaces holding the row itself to select, which was easy to fire by accident; the row keeps its tap, its swipe and its hold, which now opens its menu. (Work from the list, task-row-context-menu)
  • Today's schedule at the bottom of the sidebar. Open Schedule above your name to keep the time and the rest of today in sight while you work in a list with a task open beside it: today's timed tasks and calendar events that haven't ended, in order, with the current one highlighted. Click a task to open it in the side panel, or an event to see its details; N more today opens the Day calendar. Closed until you open it, and remembered on each device. (Schedule in the sidebar, sidebar-schedule, closes #536)
  • Add new tasks at the top, and the Add row stays in reach. Settings → Capture → Quick add → Add new tasks puts a task added from a project's or the Inbox's quick add, a section's add row, a board column, the command bar or the phone's add sheet At the top of the list instead of the bottom (the default stays the bottom). It is one choice for your account, on all your devices; lists sorted by date or priority keep their order, and subtasks, o, duplicates, templates, imports and tasks created through the API, the CLI or MCP still append. From 768 px wide the quick add now stays under the page title while a long list scrolls under it, and a task that lands out of sight says where it went (Added to the bottom of Inbox) with Show. One additive user_prefs field, add_position; no migration. (Quick add syntax, add-position, closes #540 and #541)
  • Sub-projects inherit their parent's custom fields. A field defined on a project now applies to every sub-project below it: its tasks show, edit, filter by and export the field like their own, and the sub-project's Fields… dialog lists inherited fields first, read-only. Names stay unique across the tree, the 30-field limit counts inherited fields, and the API and assistant tools follow the same rules. No migration; values are stored as before. (Custom fields, inherited-custom-fields, closes #405)

Fixed

  • Gemini and compatible endpoints say what is wrong when a test or answer fails. Google Gemini answers a wrong key with HTTP 400 and its errors as a list, so a wrong key read as "the provider refused the request", and a wrong model or base URL as a bare 404 that looked like OpenTodo's own. A rejected key now reads as a rejected key (provider_auth) for every provider, and the provider's own 404 as provider_not_found: "no such model or endpoint, check the model id" (and the base URL for a compatible endpoint). Test connection no longer sends a second request in those cases, the same rules apply on the device for encrypted accounts, and a compatible base URL pasted with /chat/completions at the end is shortened to its base. The provider's own error text is still never shown. (Set up the assistant, assistant-providers-fix, fixes #501)
  • The task menu fits on the screen. Right-clicking a task (or pressing .) opened a menu of about sixteen rows, taller than a laptop window, because every priority and every date choice had its own row. Priority… and Date… are now one item each and open their choices inside the menu; the first item or Esc goes back. The keys 1 to 4, the row's own buttons and the command palette still set them in one step. Keyboard focus also no longer jumps out of the menu while it loads.
  • Dragging a project in the sidebar shows one clear drop position, and the project stays in its workspace. The row under the pointer showed a line and an outline at the same time, so a reorder looked like making a sub-project. Now a line with a dot marks a reorder, starting at the level the project lands on, and an outline marks a drop inside. A sub-project dragged or moved to the top level in any workspace but the first also no longer jumps to the first workspace and disappears from the sidebar; one that already did can be brought back with ⋯ → Move to workspace…. (Workspaces, sidebar-project-drag, fixes #544)
  • Icon-only menu buttons show their name on hover. The Filter button of a list, the page header's More actions and the other icon buttons that open a menu (the "…" on tasks, comments and settings rows, Attach, Add reaction, More formatting) showed nothing on hover, unlike the icons beside them. They now show their name like every other icon button. (menu-icon-tooltips, fixes #528)
  • Copy buttons work on a server reached over plain http. Browsers only offer the clipboard to pages served over https or from localhost, so on a server opened at a LAN address every Copy button (the MCP URL, API tokens, invite and public links, recovery codes) silently did nothing. They now fall back to copying through a hidden text field, which works over http, and the recovery key is only marked as copied when the copy succeeded. (copy-without-https, fixes #525)
  • Short calendar items no longer overlap. In the week and day calendars, short tasks close together (13:30, 13:45, 13:50) were drawn on top of each other, because a block is drawn taller than its length to fit its title but was placed as if it were not. Blocks are now laid out by the room they take on screen: short ones that would cover each other sit side by side, longer ones that follow each other keep the full width, and the overlap marker only shows on times that really overlap. (calendar-short-blocks, fixes #526)
  • The selection bar stays in sight in a long list. Ticking tasks far down a long list used to leave "Actions" and "Clear" at the top, out of sight. The bar now stays pinned just below the page header while the list scrolls, and a row moved to with the keyboard stays below it. (sticky-selection-bar, fixes #518)
  • A JSON file without ids is no longer reported as already imported. After one custom JSON file (a converted Google Keep list, say) was imported, every later JSON file without ids showed all its rows as "already imported" and could not be imported, because rows without an id were recognised by their position. They are now recognised by their content: the same file imported again is still skipped, a different file never is. (Import, import-content-ids, fixes #520)
  • The task you select with the keyboard stays in sight. Moving up a long list with the arrow keys or k used to hide the selected task behind the page header, and the first task never brought the list back to the top. The selected row now stays just below the header, and the first task scrolls the list to the top, in project lists, the standard views, saved views and the New view page. (keep-cursor-row-visible, fixes #514)
  • A data folder the server cannot write is explained. On a fresh Unraid install Docker creates the appdata folder owned by root, and the container stopped with only "open lock file: permission denied". The message now names the user the server runs as (65532 in the image) and the chown command that fixes it, and the Unraid guide and template explain it, including how to bring back a template setting you removed. (Unraid, unwritable-data-dir, fixes #511)
  • A task moved from a project to the Inbox stays in its workspace. With several workspaces, dragging a task out of a project onto the Inbox (or moving it there with Move, the API, ot or an AI agent) used to put it in the first workspace's Inbox. It now lands in the Inbox of the workspace it was in. Tasks already moved to the wrong Inbox stay there; move them into a project of the right workspace to bring them back. (Workspaces, inbox-move-keeps-workspace, fixes #508)
  • Holding a row on a phone no longer selects its words, and the selection checkboxes sit where they belong. A slow long press used to hand the text to the browser's word selection while the row was arming: a held row is now unselectable on touch screens (the inline editor and other fields in a row still select their text), so the gesture can only mean what the app decided. The selection box is also drawn by OpenTodo instead of the platform's native control, so it fills the same 20 px slot as the completion circle at the same size on every device, rather than taking whatever size and offset the browser gave it. (Bulk actions, task-row-context-menu)
  • An event saved to OpenTodo from iOS Calendar is no longer lost. Calendar apps send events, and the CalDAV endpoint used to refuse them; iOS then dropped the item silently, and an event moved there from iCloud disappeared altogether. An event now becomes a task: due at its start, its length as the estimate, its alarms as reminders, its repeat rule where OpenTodo can express it. It then shows in Reminders and OpenTodo rather than in Calendar. (Calendar apps, caldav-events-as-tasks, fixes #500)

Changed

  • A request your reverse proxy changed now says so. An API path asked with the wrong method answers 405 with an Allow header ("GET is not allowed on /api/v1/settings/assistant/test; use POST") instead of "unknown API route", and a 404 names the method and path the server received. A 301 or 302 redirect in front of OpenTodo turns a POST into a GET; the assistant's Test connection now explains that instead of showing the raw error. (API, Behind a reverse proxy, api-method-not-allowed, refs #501)
  • The top of the sidebar shows your server's name and your workspace on two lines. With one workspace the header used to squeeze both into one line ("OpenTodo · Personal"), so a longer server name cut off the workspace. Now the name sits on the first line and the workspace below it, each with its own room. (#492, Workspaces)

0.16.0

· On GitHub

OpenTodo 0.16.0 is a security release that finishes the internal audit: adding someone to a project or workspace by email now asks them first, unless you already share something with them, and the answer no longer reveals whether an address has an account. Background work now runs when it is due: reminders at their exact time, digests at the chosen hour, deliveries right away. Please read the upgrade note for API clients.

All changes 6

Upgrade notes

  • One migration (0049_member_invitations: the table of membership invitations). No new variable. Back up first, as for every release.
  • API clients that add people by email must handle 202. POST /api/v1/projects/{id}/members and POST /api/v1/workspaces/{id}/members with an email now answer 202 {"pending": true} when the person shares no project or workspace with you yet, and also for an address without an account, which used to answer 404 user_not_found. The person is not a member until they accept, so a script that shares a project and then expects the member list to include the person must wait for the acceptance (or add someone it already shares a project with, which still answers 201). Adding by account id inside a created workspace is unchanged. (Membership invitations)

Security

  • Adding someone by email asks them first. The owner or an admin of any project could add any account on the server by its address, and the project appeared in that person's sidebar without their say; the answer also told whether the address had an account. Someone who already shares a project or workspace with you is still added at once. Anyone else gets an invitation in their notifications and in Settings → Account → Invitations (and by email when outgoing email is on) and becomes a member only by accepting it; an address without an account gets the same answer and the same "waiting for an answer" row, so the share dialog no longer tells which addresses have accounts. Declining keeps the same project from asking again for 14 days. Found in an internal audit. (Roles and assignments)

Changed

  • Background jobs run when their work is due instead of on fixed timers. On a self-hosted server: - Web push reminders go out at their firing time instead of up to 30 seconds later. - Weekly digests go out at the chosen hour instead of up to 5 minutes later. - Calendar fetches, calendar sync, issue polls and webhook retries run at their scheduled time. - Queued webhook and notification channel deliveries are sent at once. - The audit log, the Trash, old read notifications and the delivery logs are cleaned when an item passes its retention, instead of hourly or daily. - An idle server no longer checks the delivery queues every second. Self-hosted servers and hosts now run the same job definitions. (Architecture)
  • The share dialog and a workspace's members list show who has not answered yet. Invitations are listed under Waiting for an answer with their role, and can be cancelled. (Roles and assignments, Workspaces)
  • The add-member routes answer 202 for invitations. See the upgrade note above. (API)

0.15.0

· On GitHub

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.

All changes 14

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)
  • Android app behind a login page: see the new section in the Android app guide 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 <folder>, 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, #440)

Changed

  • OpenTodo moved to the OpenTodoApp organization, and the image to ghcr.io/opentodoapp/opentodo. The repository is now 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)
  • 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, The task page)
  • 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, Set up in ten minutes)
  • 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). 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)
  • 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)
  • The side panel no longer flickers when you pick the first task after changing lists (#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

· On GitHub

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.

All changes 14

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)

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)

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)
  • 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)
  • "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)
  • 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)
  • 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)
  • 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)
  • 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)
  • "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)
  • 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

· On GitHub

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.

All changes 23

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)
  • 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 <data>/opentodo.db.

Added

  • An iOS app, built from source. mobile/ios holds a Swift app around the same web app the Android app opens, with no third-party code: the first-run server screen, reminders scheduled on the phone without a push service (Snooze and Dismiss included), the share sheet, a Today widget and Face ID unlock of an encrypted account, all through the same bridge version 1, so the web app is unchanged. make ios runs its unit tests and an unsigned device build; CI builds it on a macOS runner when mobile/ios changes. The app is not in the App Store — the AGPL and the store's terms conflict — so whoever wants it builds and signs it. One limitation is honest rather than hidden: a WKWebView gets no service worker unless the server's host is known when the app is built (OPENTODO_APP_BOUND_HOST), so a portable build needs the network and only a build for one known server starts offline like the installed web app. (The iOS app)
  • 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)

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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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, Calendar sync)

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)
  • 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)

0.14.0

· On GitHub

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.

All changes 9

Upgrade notes

  • One migration (0047_calendar_sync: connections, calendars and the links between tasks and events) and a secrets file, <data>/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)
  • Calendar events in Today is a new user preference field. Devices still on 0.13 ignore it until they update. (Sync protocol)

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)
  • 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)

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)

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)
  • 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)

0.13.0

· On GitHub

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.

All changes 18

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)
  • 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)

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)
  • 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)
  • 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 <file> --workspace Work runs an import and waits for it (--dry-run shows the preview). (Import formats, #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, #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, #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, #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, #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)
  • 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, #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, #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, #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, #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, #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, #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, #359)

0.12.0

· On GitHub

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.

All changes 14

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)
  • Group mentions are stored as group:<id> 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)

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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)

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, Dokploy, Umbrel)
  • 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)

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)
  • 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

· On GitHub

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.

All changes 32

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, <data>/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)
  • 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)
  • 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)

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)
  • 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)
  • 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)
  • 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, Project timeline)
  • 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)
  • 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, All projects)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)

0.10.1

· On GitHub

A documentation fix for 0.10.0: nothing changes in the app or the server, and there is no migration.

All changes 1

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)

0.10.0

· On GitHub

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.

All changes 68

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.<account id> 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)
  • Profiles add one migration (user_profiles, email_changes); pictures are stored under <data>/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 <data>/cache/maps (at most 200 MB, not backed up). (Maps)

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-<size>.png; auth/status carries branding; changes are recorded as settings.changed. (Branding, #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, #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)
  • 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)
  • 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, Search)
  • 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)
  • 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)
  • 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), CLI)
  • 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, API)
  • 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.
  • 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:<id>, 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)
  • 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 VALARMs 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)
  • 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 <file>.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)
  • 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)
  • 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)
  • 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)
  • 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))
  • 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)
  • 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)
  • 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)
  • 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, API)
  • 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)
  • 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 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)
  • 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, Command-line client)

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, Bulk actions)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)

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)
  • 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).
  • 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, #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, #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)
  • 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)
  • 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

· On GitHub

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.

All changes 31

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)
  • # 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)
  • 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)
  • 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.
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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, Filters)
  • 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)

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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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, Settings)
  • 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, Getting started)
  • 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)

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+<commit>, the commit they were built from, in Settings → About, /healthz and the log. (Docker, #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

· On GitHub

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.

All changes 11

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)
  • 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, Rhythms)

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)
  • Statistics and the weekly digest have their own guide page, next to the weekly review's. (Statistics and the weekly digest)
  • 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, #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)
  • 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, #161)

0.7.2

· On GitHub

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.

All changes 5

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)
  • 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

· On GitHub

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.

All changes 4

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, #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

· On GitHub

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.

All changes 112

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)
  • 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)
  • 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)
  • Sync stays compatible: upgrade the server before the devices, as for every release. Changes the server makes itself now come from the device server:<id> (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, CLI)

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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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, MCP, Other calendars)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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, Podman, CapRover, Fly.io, Render, Synology)
  • 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, Cosmos, Dokploy, Easypanel, Portainer, Runtipi, TrueNAS, Umbrel)

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)
  • 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, Unraid, Coolify)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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, #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, #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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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:<id>; 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

· On GitHub

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.

All changes 7

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, 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)

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)
  • Plain-http installs on a network address work. Opened as http://<address>:<port> 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)
  • 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)

0.6.0

· On GitHub

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.

All changes 21

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.<id>, 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, Translating OpenTodo)
  • 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.<id> values); upgrade the server before the devices, as for every new field. (Custom fields, Filter queries, API)
  • 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, API)
  • 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 <name>.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)
  • 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, API)
  • 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, Habits reference)
  • 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)
  • 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)
  • 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, MCP, Command-line client)
  • 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)
  • 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)
  • 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)

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)
  • 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)

0.5.0

· On GitHub

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.

All changes 30

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, Keyboard shortcuts)
  • 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)
  • 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).
  • 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, Settings)
  • 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)
  • 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)
  • 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, Installing the app)
  • 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)
  • 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, Interface guide)

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)
  • 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)
  • 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)
  • 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)
  • 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, Command-line client)
  • 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, Sections and boards)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)
  • 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)

0.4.0

· On GitHub

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.

All changes 22

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 (docs at open-todo.app/docs) and the public demo to 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, Tasks and projects)
  • 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)

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 <cmp> <date>; 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, Views)
  • 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, Filters, Reminders)
  • 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)
  • 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, MCP)
  • 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, Things, TickTick, Linear, Import formats)
  • Site bundle for the website. Every release attaches opentodo-site-bundle-<version>.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, Docker)
  • 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))
  • 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)
  • 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)

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, API)
  • 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)
  • 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, API)

0.3.1-rc.1

· On GitHub

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

· On GitHub

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 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.

All changes 35

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)
  • 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/<token> 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, API)
  • 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 <data>/secrets/assistant.json, never returned, logged, exported or backed up. (AI assistant)
  • 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)
  • 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 <data>/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, API, security)
  • 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): --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)
  • 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)
  • 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)
  • 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)
  • 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 <name> --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)
  • 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)
  • 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)
  • 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)
  • /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+<commit> 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

· On GitHub

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.

All changes 18

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, Coolify)
  • 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)

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 website at 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 <topic> 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)
  • 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)
  • 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)
  • 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)

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)
  • 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)

0.1.0

· On GitHub

The first release, and the first feature-complete version: every item on the roadmap 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.

All changes 72

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.
  • 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

  • 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, sync protocol
  • 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, proxy notes
  • Vim keybindings, opt-in per device and loaded only when enabled.
  • 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
  • Voice capture into quick add (Shift+V), opt-in per device, using the browser's speech recognition. Voice
  • Share target, bookmarklet and a Manifest V3 browser extension for Chromium and Firefox (make extension), opening a capture screen at /share. Capture
  • Email to task: a personal secret address per account, polled from one operator-configured IMAP mailbox (OPENTODO_EMAIL_*). Email
  • ot command-line client (cmd/ot), cross-compiled for Linux, macOS and Windows and attached to releases. CLI
  • 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
  • Manual ordering by drag and drop and Alt+↑/↓, one position op per move. Manual order
  • Labels as a synced entity referenced by id from task.labels. 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
  • 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
  • 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, Purge
  • 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
  • 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 <data>/push_vapid.pem, OPENTODO_PUSH=off to disable. Reminders
  • 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
  • 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, API
  • Task assignment (assignee_id), the Assigned to me view and in-app notifications (GET /api/v1/notifications). Assignment
  • Threaded Markdown comments (comment entity) with @mentions, and an activity log derived from the op log, per task and per project. Comments
  • Activity feed across shared projects with unread counts (project_prefs entity, personal scope only; GET /api/v1/activity). Activity feed
  • Invitations: single-use, expiring links in Settings → Members. 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
  • 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
  • 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
  • Signed outbound webhooks with a durable queue, retries, delivery log and SSRF protection (OPENTODO_WEBHOOKS_ALLOW_PRIVATE preset). Webhooks
  • 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
  • Read-only MCP endpoint at /mcp for AI assistants (five tools, opentodo:// resources), off until the owner enables it (OPENTODO_MCP preset). MCP
  • 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
  • Audit log of security events with retention, NDJSON export and opt-in client addresses. What is logged, API
  • 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
  • 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

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
  • 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
  • /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
  • 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
  • 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
  • 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 <data>/secrets/ (mode 0600), outside the database and backups.
  • See SECURITY.md for the full model.

Known limitations and verification follow-ups

  • 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).
  • Touch drag and drop (board, manual ordering, calendar) on real touch devices.

Migrations

    Read the code.

    OpenTodo on GitHub