# Changelog All notable changes to OpenTodo are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] ### Fixed - **The `latest` image reports which build it is.** Images built from the main branch reported their version as `main`; they now report `dev+`, the commit they were built from, in Settings → About, `/healthz` and the log. ([Docker](docs/self-hosting/docker.md), #160) - **No "Open menu" bubble over the menu on phones.** Tapping an icon button (such as the menu button) no longer leaves its tooltip on screen, where it covered the first sidebar entry; tooltips still appear on hover and when you move to a button with the keyboard. (#169) ## [0.8.0] - 2026-10-18 OpenTodo 0.8.0 introduces **Open Loops**, a method of its own for getting everything off your mind, inspired by GTD: catch, clarify, place, do and review. You choose how prominent it is, from not at all to the first screen you see. The weekly review becomes a guided walk through your Inbox, what slipped, what is waiting and every project, joined by a five-minute Daily check and a Monthly look. Nothing changes until you turn it on. Email to task also keeps long messages and turns newsletters into readable, formatted notes. 0.7.2 was prepared but not published on its own: its changes (listed below) arrive with this release. ### Upgrade notes - **No migration and no new environment variables.** Replace the image or the binary and restart. - **Sync is additive:** new preference fields (`user_prefs.method_mode`, `project_prefs.reviewed_at`, and `daily_at`, `monthly_at`, `focus_date` and `focus` on the review row) hold settings and timestamps, never task content; older apps ignore them. Upgrade the server before the devices, as for every release. - **The JSON export now includes per-project preferences** (`project_prefs`, such as when you last reviewed a project), and importing your own export restores them. - **The weekly email digest lists fewer projects as "without a next action"**: see Changed. ### Added - **Open Loops, a calm method for everything on your mind.** OpenTodo now has a method of its own, inspired by GTD: catch, clarify, place, do and review. Choose how prominent it is in **Settings → Account → How you work**: **Lists only** (as before), **Companion** (screens name the move they serve and link to the method) or **Open Loops first** (the new **Loop home** opens first and the sidebar is arranged by the Places). New views: **Next** (g Shift+N), each project's next action grouped by `@context` labels, and **Waiting** (g Shift+W), what you assigned to someone else or is blocked. The method has its own chapter in the docs and Help. ([Open Loops](docs/method/overview.md)) - **The weekly review walks every project, one step at a time.** A stepper shows where you are and what is left; **Next** (]) and **Back** ([) move through seven steps in three phases: the Inbox and a Mind sweep, then what slipped, what is waiting, each project one card at a time (r marks it reviewed, q adds a next action), the week ahead, and Someday. Your mark on a shared project is yours alone. A five-minute **Daily check** (g Shift+R) picks up to three tasks as today's focus, and a **Monthly look** covers your areas, old Someday items and the four-week trend. ([Weekly review](docs/review-and-statistics.md), [Rhythms](docs/method/rhythms.md)) ### Changed - **"Projects without a next action" counts waiting and deferred tasks.** In the review and the weekly email digest, a project whose open tasks are all assigned to someone else, blocked, or start later is no longer listed; one whose only tasks are parked in Someday now is. ([Places](docs/method/places.md#projects-without-a-next-action)) - **Statistics and the weekly digest have their own guide page**, next to the weekly review's. ([Statistics and the weekly digest](docs/statistics.md)) - **Why OpenTodo can be missing from the Android share menu.** The capture guide explains that Android lists only web apps installed as an app from Chrome or Samsung Internet, how to check, and what works instead. ([Capturing pages](docs/capture.md#opentodo-is-missing-from-the-share-menu-on-android), #159) ### Fixed - **Newsletters sent to Email to task are readable.** An HTML message now becomes formatted notes with its headings, lists, bold text and links shown as their text, instead of plain text with every tracking address spelled out; a newsletter's hidden preview text and its blank filler lines are gone. ([Email to task](docs/email.md)) - **Email to task keeps long messages.** A forwarded message's text was cut at 16 KB; the task now keeps about 48 KB, as much as its notes hold (also with end-to-end encryption), and only longer messages end with a note that the rest did not fit. ([Email to task](docs/email.md), #161) ## [0.7.2] - 2026-10-18 OpenTodo 0.7.2 gives the sidebar on a computer more room for your projects: More opens as a menu, Help joins the footer icons, and empty groups stay hidden until you use them. The owner of a new server is greeted with a short thank-you, and a new **Settings → About** section gathers the version, the documentation, the website, the issue tracker and the source code. Unlike 0.7.1, its GitHub release has the binaries attached again. ### Upgrade notes - **No migration and no new environment variables.** Replace the image or the binary and restart. - **Sync is additive:** one new preference field (`user_prefs.welcome`) remembers that the welcome note was closed; older apps ignore it. ### Added - **A thank-you for new servers.** The first time the owner opens a new server, a short note thanks them for installing OpenTodo and links to the issue tracker, the GitHub repository and Buy Me a Coffee. It shows once per account and never to invited members or on the demo. ([Feedback and support](docs/feedback-and-support.md)) - **Settings → About.** A new section for everyone with the server's version, Help, the documentation and the website, the issue tracker, the source code and a Buy Me a Coffee button; the owner can show the welcome note again from there. ### Changed - **A roomier sidebar on a computer.** Search sits next to the OpenTodo name, Help joins the icons in one footer row, and **More** opens as a menu instead of pushing your projects down. Filters and Labels appear once you have one, rows are a little tighter with a mouse, and the sidebar has a thin scrollbar with a soft fade where more is hidden. In a 1280×600 window seven projects now fit instead of two. Touch screens keep their larger targets and the inline More group. ## [0.7.1] - 2026-10-18 OpenTodo 0.7.1 lets a project keep its completed tasks in its own list, in a Completed group you open when you need it, for lists you tick off and reuse such as groceries. It also fixes completed tasks showing an empty circle instead of a ticked one. ### Upgrade notes - **No migration, no new environment variables, no sync or API change.** Replace the image or the binary and restart; `stable` and `:0.7` already point at 0.7.1. - **The GitHub release of 0.7.1 has no attached binaries** (the tag was pushed before this changelog section existed, so the release job stopped after publishing the images). Use the container image, or 0.7.2 for binaries. ### Added - **Keep completed tasks in a project.** Turn on **Show completed tasks** in a project's menu and its completed tasks stay at the end of the list in a **Completed** group, closed until you open it, newest first. Untick one there to put it back on the list, handy for groceries and other lists you reuse. The setting is per project and per device. ([Completed tasks in a project](docs/completed-in-projects.md), #145) ### Fixed - **Completed tasks show a ticked circle again.** In the Completed view, a section's revealed completed tasks and the task panel, a completed task's circle kept its priority colour, which hid the check mark. ## [0.7.0] - 2026-10-18 OpenTodo 0.7.0 is a hardening release, the result of a review of the whole code base. Your account is harder to take over: credential and administration changes need an admin token or your signed-in session, repeated wrong passwords lock password sign-in for a while, a new server cannot be claimed from the internet without a setup code, forwarded addresses count only from trusted proxies, and **Sign out everywhere** ends every session and token at once. Sync keeps your changes in more of the situations where it could lose them (a device with a wrong clock, a proxy answering in the server's place, signing out during a sync, the history of a newly joined project), large servers and long lists answer faster, backups no longer fill the disk and reach S3 above 5 GB, and the app works better with screen readers and voice control. Templates for fourteen more platforms and a `stable` image tag make installing and updating simpler. Read the upgrade notes first: emailed links now need `OPENTODO_BASE_URL`, and scripts that change credentials need an admin token. ### Upgrade notes - **Migrations `0035` to `0038`** (attachments of projects shared earlier move to the project; the `admin` token scope; confirmed pages of a joined project's history; an index for dependencies) are applied automatically at startup. Back up `/data` before upgrading: there are no down-migrations. - **No new environment variable is required, but set `OPENTODO_BASE_URL`** if you have not. Password resets, sign-up confirmations and invitations are emailed only when it is set, and with it set the app must be opened at its host (the sign-in screen names the expected address otherwise). With an https base URL the server also sends HSTS. ([Outgoing email](docs/self-hosting/outgoing-email.md#set-the-base-url-first)) - **Reverse proxies and client addresses.** Proxies on loopback, private or container networks, and `OPENTODO_TRUSTED_PROXIES=cloudflare`, need nothing. If you set `OPENTODO_TRUSTED_PROXIES` to your own addresses, the list now replaces the default: add the new keyword `private` if a reverse proxy on the machine or a container network also connects (for example `private, 203.0.113.10`); a list without any local address shows a configuration warning. A reverse proxy on a public address must be listed, otherwise every visitor shares its address. Behind `OPENTODO_BASE_URL`, open the app at the base URL's host. - **A new server reached from the internet asks for a setup code** before the owner account is created, unless `OPENTODO_SETUP_TOKEN` is set: it is printed in the log (`docker logs opentodo`). From the same machine or your local network nothing changes, and servers that already have an owner are not affected. ([The setup code](docs/self-hosting/docker.md#the-setup-code)) - **API clients and scripts.** A `full` token is refused on the credential and administration routes with `403 insufficient_scope`: use an `admin` token there (see Breaking). Request bodies are checked strictly, so a field a route does not define is refused with `400 bad_request`. Sign-in answers `429 account_locked` after repeated wrong passwords, and password checks can answer `503 busy`. ([API](docs/api.md#behaviour-notes)) - **Sync** stays compatible: upgrade the server before the devices, as for every release. Changes the server makes itself now come from the device `server:` (one id per server process; webhooks still report the source `server`). Apps from 0.7.0 confirm each page of a joined project's history and correct their clock when the server asks; older apps keep the previous behaviour. One new route: `POST /api/v1/me/sign-out-everywhere`. - **Give the container time to stop.** On shutdown the server waits up to 20 seconds for its background jobs. Docker stops a container after 10 seconds by default, so set `stop_grace_period: 30s` in your compose file (or `docker stop -t 30`) to let a backup or a digest finish cleanly. - **Image tags.** The release publishes `ghcr.io/pixari/opentodo:0.7.0` and `:0.7` for `linux/amd64` and `linux/arm64`, and moves `:stable`, which most templates and `docker-compose.yml` now follow. Installs on `:0.6` stay on 0.6: change the tag to `stable` (or `:0.7`) to update. The templates for stores that need a version (CapRover, CasaOS, Easypanel, Helm, Runtipi, TrueNAS, Umbrel) name `0.7.0`. - **Binary size** is about 28 MB (27 MB in 0.6); the installed app's precache stays at about 4.4 MiB. ### Breaking - **Scripts that use a `full` token on credential or administration routes must switch to an `admin` token** (breaking for those scripts only; everything else, including reading these settings and lists, revoking tokens and app passwords, and forgetting remembered browsers, works with `full` tokens as before). Such requests now get `403 insufficient_scope`. Create an admin token in **Settings → Integrations → API tokens → New token**, access **Admin** (after confirming your password if asked), or `POST /api/v1/tokens` with `{"scope":"admin"}` from a signed-in session, and put it in the script instead. `ot token create` needs an admin token in the profile too; `ot login` still works. Existing tokens keep their scope. ([API](docs/api.md#token-scopes), [CLI](docs/cli.md#tokens-and-export)) ### Security - **A server can no longer turn end-to-end encryption off on your devices.** If the server says it has no encryption key record (for example after it was restored from an older backup), your devices keep encrypting and pause syncing instead of uploading your content in plaintext. A screen lets you restore the key record from the device, turn encryption off on that device, or sign out. When you turn encryption off on one device, each of your other devices now asks once before following. ([End-to-end encryption](docs/e2ee.md)) - **Encryption key records with unusual settings are refused.** A device only unlocks with a key record whose key derivation strength is within the expected range; otherwise it stays locked and says why. - **Only the share sheet can hand files to the installed app.** Other websites can no longer store shares in the app through its share target, and a share larger than 100 MB in total is not stored. ([Capture](docs/capture.md)) - **The encryption threat model now notes that the server can show an older version of an encrypted field.** Content stays unreadable and unforgeable to the server, but the app cannot tell when it is served a previous encrypted value of the same field. ([End-to-end encryption](docs/e2ee.md)) - **Pasting into notes is safer.** The rich-text editor's ProseMirror view is updated to 1.42.6, which fixes a published issue with pasted content (GHSA-c8x8-7fp4-3x9w). - **Links are encrypted with the rest of your data.** Turning end-to-end encryption on now also encrypts the address and name of links you attached before, and turning it off decrypts them again. Each link is replaced by a copy in the new form; the old one moves to the Trash. ([End-to-end encryption](docs/e2ee.md)) - **A damaged or tampered backup archive cannot fill the disk during a restore.** Every file in the archive must have exactly the size its manifest lists, nothing more is written, and the server checks that the listed files fit before it extracts anything. ([Backups](docs/backups.md)) - **Images and release files can be verified.** The container image is signed with cosign (keyless, tied to the release workflow) and carries an SBOM and build provenance; every file on a GitHub release has a build provenance attestation for `gh attestation verify`. ([Running with Docker](docs/self-hosting/docker.md#verifying-the-image-and-downloads)) - **A release is published only when every check passed.** The image, its `stable` tag and the GitHub release now wait for the layout, interface, docs website and template checks too; the image is built, scanned for known vulnerabilities (a critical one with a fix available stops it) and smoke-tested before it is pushed. Build tools and base images are pinned to exact versions, and Dependabot proposes updates weekly. - **Release binaries and the image are built with Go 1.27.2**, which fixes standard-library vulnerabilities (net/http, crypto/tls, crypto/x509 and others) in the 1.26.0 toolchain used up to 0.6. The `go.mod` toolchain, the Docker build stage and CI use the same version, the image's web app is built on Node 26, and CI fails when the code can reach a known vulnerability (`govulncheck`). People building from source still need Go 1.26 or newer; the `go` command downloads 1.27.2 for them. - **Security reports go through GitHub's private vulnerability reporting.** See `SECURITY.md`. - **Forwarded client addresses count only from trusted proxies.** `X-Forwarded-For` (and Cloudflare's `CF-Connecting-IP`) are believed only when the connection comes from a trusted proxy: by default loopback, private and link-local addresses, which covers a reverse proxy on the same machine or a container network. A client that reaches the server directly can no longer choose the address that rate limits, the audit log and the demo limit see. IPv6 clients are limited per `/64`. ([Behind a reverse proxy](docs/self-hosting/docker.md#behind-a-reverse-proxy)) - **Repeated wrong passwords lock the account for a while.** Ten failed passwords, two-factor codes or re-authentications for one account within 15 minutes, from any addresses, lock its password sign-in for the rest of those 15 minutes (the app says so; the API answers 429 `account_locked` with `Retry-After`). A successful sign-in clears the count. Passkeys, single sign-on and existing sessions keep working. - **A new server cannot be claimed from the internet without a code.** Without `OPENTODO_SETUP_TOKEN`, creating the owner account from a public address now needs a one-time setup code that the server prints in its log when it starts without an owner (`docker logs opentodo`). From the same machine or your local network nothing changes. The code is forgotten once the owner exists. ([The setup code](docs/self-hosting/docker.md#the-setup-code)) - **Stricter origin check with a base URL.** With `OPENTODO_BASE_URL` set, browser requests must be addressed to its host, so a page under another name that resolves to your server is treated as cross-site. - **Secure cookies over https, and HSTS.** Cookies are marked `Secure` whenever the request arrived over https, also without a base URL. With an https `OPENTODO_BASE_URL`, responses send `Strict-Transport-Security` for one year (no `includeSubDomains`, no preload). - **Password checks can no longer exhaust a small server.** Registration, project invite links and password resets check the sign-up policy, the invitation or link and the setup token before the password is hashed; at most twice the CPU count of password checks run at once, and a request that waits more than a second gets 503 `busy` with `Retry-After`. Sign-in refuses passwords over 1,024 bytes without checking them. ([API](docs/api.md#behaviour-notes)) - **Sign-in timing no longer tells accounts without a password apart** (single sign-on and demo accounts): they cost the same check as an unknown address. - **Live-update streams are capped:** 10 per account (a new one closes the oldest), 20 per client address and 2,000 per server; beyond the last two the stream is refused with 503 `busy` and the app keeps syncing by polling. ([Sync](docs/sync.md#live-updates)) - **One account's push endpoints cannot hold back everyone's reminders.** Reminders are sent for 8 accounts at a time with a 20 s budget per account and pass, an account keeps at most 20 push subscriptions (a new one replaces the least recently registered), and a subscription that failed 20 times in a row and delivered nothing for a day is no longer sent to until the device re-enables notifications. ([API](docs/api.md#reminders-and-push)) - **Calendar data, MCP batches and calendar subscriptions are bounded.** Reading iCalendar data (CalDAV writes and subscribed calendars) takes time in proportion to its size, and one content line may be at most 64 KB. The MCP endpoint refuses JSON-RPC batches under protocol revisions that no longer have them (2025-06-18 and later), accepts at most 20 messages per batch otherwise, and stops running a batch once its answer reaches 4 MB. Expanding a subscribed calendar's repeating events starts at the shown period, shares one work budget and a 5-second deadline per calendar, and keeps what it found with a warning when it runs out. ([CalDAV](docs/caldav.md), [MCP](docs/mcp.md#limits-and-security), [Other calendars](docs/other-calendars.md#limits)) - **Emailed links always use the server's public address.** Password resets, sign-up confirmations and invitations sent by email are built only from `OPENTODO_BASE_URL`, never from the request, so nobody can make the server mail a link to another site. Without `OPENTODO_BASE_URL` these emails are not sent: the screen says the owner must set it, links to copy keep working, and the owner sees a configuration warning while outgoing email is set up. ([Outgoing email](docs/self-hosting/outgoing-email.md#set-the-base-url-first)) - **Forwarded host headers count only from trusted proxies.** `X-Forwarded-Host` and `X-Forwarded-Proto` are used only when the request comes from an address in `OPENTODO_TRUSTED_PROXIES`. Behind a TLS proxy, set `OPENTODO_BASE_URL` (or list the proxy) so the links the app shows start with `https://`. ([Docker](docs/self-hosting/docker.md#behind-a-reverse-proxy)) - **A shared project's id cannot be taken over.** Sharing a project is refused when another account has a project with the same id, so nobody can lock the owner out of their own project. ([Sync](docs/sync.md)) - **Entity ids are limited to safe characters.** Letters, digits and `. _ : @ + = ~ -`, which covers every id the apps create, so a malformed id can no longer break the lists of a shared project's members. ([Sync](docs/sync.md#operation)) - **Credentials and administration need an admin token or your signed-in session.** A `full` API token keeps everything it could do, except: turning two-factor authentication on or off, recovery codes, passkeys, creating API tokens and app passwords, unlinking single sign-on, changing the password, invitations, user administration (roles, deactivation, deletion, reset links, ownership transfer, two-factor resets), the two-factor policy, single sign-on settings, the encryption key record, the outgoing-email, backup and assistant settings that hold secrets, and a hosted server's whole-server archive download. Those now take a session (re-confirmed in the last ten minutes where they already asked for that) or a token with the new **`admin`** scope, which can only be created from such a session. A leaked `full` token can no longer turn off your second factor, mint more credentials or issue reset links. ([API tokens](docs/api.md#token-scopes)) - **Single sign-on never links the owner, admins or accounts with two-factor authentication or a passkey by email alone.** Those accounts sign in as usual and link the provider in Settings → Account → Linked accounts; until then the provider sign-in is refused with "This account is not linked yet". Members without a second factor are still linked by their verified email. ([Single sign-on](docs/self-hosting/sso.md)) - **A password reset forgets remembered browsers**, so every browser asks for the two-factor code again, whether the link came from **Forgot password?** or from an administrator. - **Sign out everywhere.** Settings → Account → Session → **Sign out everywhere** (`POST /api/v1/me/sign-out-everywhere`) ends every session of your account, this one included, forgets remembered browsers and revokes your API tokens and CalDAV app passwords at once. It is recorded in the audit log as `sessions.revoked`. ([Account security](docs/account-security.md#sign-out)) - **Every route declares who may call it.** Each API route is registered with one policy (anonymous with a reason, signed in, owner, owner and admins, accepted token scopes, re-confirmed session, refused on a demo), replacing the separate owner and session checks that disagreed (outgoing email refused tokens while single sign-on accepted them; both now follow the rule above). A test fails for any route without a policy and checks the demo restrictions against it. ### Added - **Templates for six more ways to run OpenTodo.** A Helm chart for Kubernetes (one pod with a persistent volume, an optional Ingress, a generated setup token, a locked-down pod), published with each release to `oci://ghcr.io/pixari/charts/opentodo`; a Podman Quadlet that runs OpenTodo as a systemd service, kept up to date by `podman auto-update`; a CapRover one-click app; a `fly.toml` for Fly.io; a Render Blueprint with a Deploy to Render button; and a Container Manager project for Synology DSM 7.2+. Each is checked by `make check` and has a guide. ([Kubernetes](docs/self-hosting/kubernetes.md), [Podman](docs/self-hosting/podman.md), [CapRover](docs/self-hosting/caprover.md), [Fly.io](docs/self-hosting/fly.md), [Render](docs/self-hosting/render.md), [Synology](docs/self-hosting/synology.md)) - **Templates for eight more platforms.** CasaOS and ZimaOS, Cosmos Cloud, Dokploy, Easypanel, Portainer, Runtipi, TrueNAS Community Edition and Umbrel each get a template in their own format, checked by `make check`, with a guide covering install, settings, data permissions, https, updates and backups. Until a platform's catalog lists OpenTodo, the guide shows how to install it as a custom app; Portainer and Dokploy can already load the templates from this repository by URL. Stores that refuse moving tags name the release version (Umbrel also its digest, refreshed by `scripts/deploy/pin.mjs`); the others use `stable`. ([CasaOS](docs/self-hosting/casaos.md), [Cosmos](docs/self-hosting/cosmos.md), [Dokploy](docs/self-hosting/dokploy.md), [Easypanel](docs/self-hosting/easypanel.md), [Portainer](docs/self-hosting/portainer.md), [Runtipi](docs/self-hosting/runtipi.md), [TrueNAS](docs/self-hosting/truenas.md), [Umbrel](docs/self-hosting/umbrel.md)) ### Changed - **The task list, calendar, task properties and assistant panel are built from smaller parts.** Nothing changes in how they look or work; their grouping rules now have their own tests, and a check keeps new screens from growing into one very long function again. - **For contributors: the deploy template checks share one strict YAML parser** (`scripts/deploy/yaml.mjs`) instead of four that read some files differently, so a quoted `'it''s'` or a folded text means the same in every check. The committed templates are unchanged. - **For contributors: `AGENTS.md` and `docs/architecture.md` name every server package**, `AGENTS.md` describes the MCP server's write tools, and a repository check fails when a package or token scope is left out. The privacy list in the README now also names outgoing email and calendar subscriptions. - **OpenTodo is in Unraid's Community Applications.** Install it from the **Apps** tab: search for OpenTodo and click **Install**. Installing from the template URL by hand still works. ([Unraid](docs/self-hosting/unraid.md)) - **A `stable` image tag, used by every template.** Each release now also publishes `ghcr.io/pixari/opentodo:stable` (release candidates never move it), and the Unraid and Coolify templates, `docker-compose.yml` and the README examples use it: every release, minor versions included, arrives through the platform's update button, with migrations applied on start. Installs on `:0.6` keep their tag and its fixes; switch to `stable` once to follow every release. A **Promote stable** workflow points `stable` at an earlier release to roll back. ([Docker](docs/self-hosting/docker.md), [Unraid](docs/self-hosting/unraid.md#updates-and-backups), [Coolify](docs/self-hosting/coolify.md)) - **Choose the address `docker-compose.yml` publishes on** with the optional `OPENTODO_BIND` in `.env`. The default stays every interface, so existing installs reached from your network keep working; behind a reverse proxy on the same machine, `OPENTODO_BIND=127.0.0.1` is the safer choice. ([Docker](docs/self-hosting/docker.md)) - **Contributors: the server code is easier to find your way in.** The HTTP server, the sync write path, quick add, the importer, CalDAV and the AI agent tools are split into smaller files and functions, with no change in behaviour. A test now fails when a function under `internal/` grows past 150 lines or a file past 1,100 lines. - **Separate rate limits per purpose.** Sign-in, registration, password resets, invitation links, public links, MCP token failures and passkey sign-in each have their own window of 10 a minute per address, and successful sign-ins no longer count, so guessing one kind of link no longer blocks signing in. - **Request bodies are checked strictly.** A field a route does not define is refused with 400 `bad_request` naming it (the sync exchange, push subscriptions and passkey credentials still accept extra fields), data after the JSON value is refused, a body over the size limit gets 413 `too_large`, and error messages no longer show the server's internal decoder text. The app and the `ot` CLI send only defined fields. ([API](docs/api.md#behaviour-notes)) - The last-use time of API tokens and app passwords is updated at most once a minute, which saves a database write on every request. - The outgoing-email settings (`GET`/`PUT /api/v1/settings/smtp`) now accept an `admin` token as well as the owner's session, like the other settings that hold secrets; other tokens are still refused. ### Fixed - **The phone sidebar scrolls with More open.** On a short phone screen, opening More left the sidebar almost no room to scroll, so a swipe moved the page behind it instead of reaching your projects. More now scrolls with Filters, Projects and Labels, and the page behind the open sidebar stays still. - **The quick add sheet treats archived projects like the views' quick add.** Typing `#name` of an archived project no longer behaves differently in the sheet and in a list; every screen now reads projects, labels and shared-project roles from one shared live copy, so they agree with each other and update together after a sync. - **Screen readers hear what the chips say.** Counts, "Has notes", deadlines, group counts and the warning on a title that could not be decrypted were silent or read as bare numbers; they are now read as full phrases ("3 comments", "4 open tasks"). - **Screen readers follow the list.** Moving with j / k moves focus to the selected task, subtasks are read with their level and position, and each complete button names its task ("Complete Buy milk"). ([Keyboard shortcuts](docs/keyboard.md)) - **Page changes are announced.** The browser tab names the page ("Today – OpenTodo"), going to another page moves focus to its heading, and a "Skip to content" link jumps past the sidebar. ([Keyboard shortcuts](docs/keyboard.md)) - **The log says which level it uses.** The startup line now ends with `log_level=`, and with `OPENTODO_LOG_LEVEL=debug` a first debug line says what debug adds (one line per API request). Before, a log read right after a restart looked the same at `debug` as at `info`, so the setting seemed to be ignored. (#128) - **Project pickers no longer list projects twice.** With only a few projects, a picker (such as "Tasks without a project go to" when importing) showed the ones picked before under "Recent" and then again in the full list. Short lists now show each project once; longer lists keep "Recent" on top and head the full list "All". (#136) - **Buttons and menu items are named by the text they show** (WCAG 2.5.3 Label in Name), so voice control finds them by what is on screen: menu items no longer repeat their shortcut or note in the name, group headers are named by their title and count (collapsed or expanded is announced as a state), calendar items, month days and the project's member list read their full description without hiding the visible text, and "Make owner" and "Restore this value" start with their visible words. - **Unraid: Tailscale explained.** The container's **Use Tailscale** switch cannot work with the OpenTodo image (it has no shell for Unraid's Tailscale script, so the container stopped with `exec /opt/unraid/tailscale: no such file or directory`). The Unraid template and guide now say to leave it off and give the steps for Tailscale Serve on the Unraid server, which gives OpenTodo an https address on your tailnet. ([Unraid](docs/self-hosting/unraid.md#tailscale), #123) - **A server opened at an address other than its base URL says so.** After a move to a new domain with `OPENTODO_BASE_URL` still naming the old one, signing in and "Try the demo" were refused with "cross-site request rejected" or "This page expired. Reload it", which a reload does not fix. The sign-in screen now names the address the server is set up for, and the log has one warning per refused host naming it and the base URL. ([Behind a reverse proxy](docs/self-hosting/docker.md#behind-a-reverse-proxy), #125) - **A proxy page no longer loses unsynced changes.** When something between the app and the server (a proxy, a captive portal) answered a sync with its own page, changes made offline were dropped. They now stay on the device and upload on the next sync, and the status says the answer was not understood. - **Changes are kept when a sync turns out to need unlocking.** If encryption was turned on on another device during a sync, changes a server limit held back were lost and refused changes kept their local values. They are now held for later or take the server's values. - **Custom fields survive a server restore.** After the server is restored from a backup, devices now send their custom field definitions back with everything else. - **Refused custom field and link changes are handled.** A custom field change the server refused now shows the server's value, and a refused attachment no longer stops syncing. - **Stopping the server no longer interrupts its background work halfway.** On shutdown the server now waits (up to 20 seconds) for its background jobs before closing the database: a backup in progress is cancelled and recorded as failed instead of failing with "database is closed", and a weekly digest or reminder that went out just before shutdown is recorded, so it is not sent a second time after the restart. - **Backups no longer fill the disk.** A backup checks the free space first and fails at once with "not enough free disk space for a backup" and the space it needs. Snapshots and partial archives left by a crash are removed when the server starts, and backups to a folder on the server are written straight into that folder instead of being copied there from the data directory. ([Backups](docs/backups.md)) - **Email to task leaves other mail clients' deleted messages alone.** On a mail server without the UIDPLUS extension, OpenTodo no longer expunges the mailbox (which removed every message marked for deletion, not just its own); processed messages stay marked read and deleted until the mail server or your client removes them. ([Email to task](docs/email.md)) - **Reminders on completed tasks no longer slow the server down over time.** The server records them as dismissed once instead of checking them again every 30 seconds, and reads due reminders in pages. ([Reminders](docs/reminders.md)) - **Large backups reach S3 and slow destinations.** Archives over 100 MiB are uploaded to S3 in parts, so archives over S3's 5 GB single-upload limit work, and an upload is no longer cut off after 30 minutes: it fails only when the destination stops answering for 5 minutes. Give the S3 key `s3:AbortMultipartUpload` too. ([Backups](docs/backups.md)) - **Old read notifications are removed while the server runs.** Read notifications older than 90 days are now removed daily, not only when the server starts. - **Signing out during a sync no longer leaves the previous account's data behind.** A sync still waiting for the server is stopped before it writes anything more, and the next account starts with a fresh device. Other open tabs of the app reload to the sign-in screen. ([Offline and sync](docs/offline-and-sync.md)) - **A server that stops answering no longer stalls syncing.** Every request now gives up after 30 seconds (10 minutes for file uploads and downloads); the status shows offline and the app tries again shortly, waiting a little longer each time. ([Offline and sync](docs/offline-and-sync.md)) - **Open tabs recover after a server update.** A screen whose code changed in the update reloads on its own, a "Reload to update" prompt appears when a new version is ready or another tab updated the stored data, and an error in one screen or panel no longer blanks the whole app. The server now answers missing app files with "not found" instead of the app page, and only a real network problem shows as offline. ([Troubleshooting](docs/troubleshooting-app.md)) - **Changes that can't be made say why.** Moving a task under one of its own subtasks, a move a section does not allow, or a change that fails to save now shows the reason at the bottom of the screen instead of doing nothing. - **Only one tab syncs.** With the app open in several tabs, one of them syncs for all; the others show its status and take over when it closes. Moving between lists no longer starts a new sync each time. ([Sync protocol](docs/sync.md)) - **Changes held back by a server limit are no longer lost or sent twice** when they are offered again at the moment another change is held. - **A title you are typing stays as you type it** when another device renames the task at the same time; leaving the field saves your title. - **The browser is asked to keep the app's data**, so it is not deleted when space runs low or after weeks without a visit. - **A device with a wrong clock no longer freezes a field.** When a device's clock runs more than five minutes fast, the server now asks it to correct its clock; the app stamps the change again and uploads it, so the edit is kept and later edits from other devices win as they should. The app also stops correcting its clock once the system clock is right again. Older apps keep the previous behaviour. ([sync](docs/sync.md#clocks)) - **Two server writes in the same millisecond no longer cancel each other.** Every change the server makes itself (API, assistant, reminders, imports, email) now uses one clock, and the server's device id carries a suffix per server process (`server:`; webhooks still report the source `server`). - **History of a newly joined project is resent when a page is lost.** The app confirms each page of a shared project's history it applied, and the server sends a page again until it is confirmed. - **Deleting an account unassigns its tasks in other people's projects**, as when a member leaves, so no task keeps a deleted person as its assignee. - **Notifications and webhooks are no longer lost when the app disconnects during a sync.** Once a change is saved, its notifications and webhook deliveries are recorded even if the request was cut off. - **Impossible due dates are refused.** A due date that does not exist (such as 31 February) or a time outside 00:00–23:59 is no longer accepted, so it cannot break recurring tasks and reminders. - **A database migration that would have been skipped is reported.** The server records which migration files it applied and logs an error at startup for a file it never ran; the repository checks refuse a new migration not numbered above those on the main branch. - **Lists and syncs no longer wait for slow writes.** The server reads through a small pool of read-only database connections next to its single writer, so task lists, syncs and exports answer while a large import, purge or backup step is being saved. A second server process on the same data directory (during a rolling update) now waits its turn to write instead of failing with "database is locked". - **Joining a shared project no longer slows down your lists and first sync.** On a server with many users, listing tasks and the first sync of a member of a shared project read every user's data; they now read only the projects you can see (on a 300,000-change server, a task list went from about 460 ms to 54 ms, or under 4 ms for a member with little data of their own). The server also refreshes its database statistics at startup and daily. - **Saving many dependencies at once no longer stalls the server.** A sync that adds hundreds of "blocked by" links to a large project used to hold the database for many seconds (about 10 s for 1,000 links on 10,000 tasks), delaying everyone else; it now takes a fraction of a second. Completing a task also checks for unblocked tasks faster. A new database index is created on start. - **The activity feed loads faster on projects with a long history.** Each page now reads only the recently changed tasks it needs, and never the notes documents, instead of the whole history of every shared project. The events shown are the same. - **Large public project pages, syncs for members of many projects and CalDAV clients cost the server less.** A public link view no longer runs one query per task, listing memberships no longer runs one query per project, and CalDAV multiget and calendar queries no longer scan the whole account for each task asked for. - **A new hosted instance always gets its setup link.** In multi-tenant hosting, creating an instance whose record could not be read back right away replied without its setup token; the reply now keeps the record just created. - **More reliable tests and checks (for contributors).** Every API test now checks the status of each response, `make check` and CI run staticcheck, the server and the web app share one list of synced and encrypted fields that both test suites check, and a randomized test with replayable seeds checks that devices with wrong clocks converge after a project is shared. The demo's "busy" API message now starts in lower case like the others; the app shows its own text. - **Snooze and Dismiss from a notification follow the corrected clock.** On a device whose clock is off, the notification actions now stamp their change with the same server-corrected time as the app, so the change is neither overwritten by an older edit nor refused; a change the server does refuse stays queued for the app's next sync instead of being dropped. - **Enter in a filter moves to its results right after the view opens.** Pressing Enter before the view had read your tasks left focus in the query field; it now moves to the first result once they are loaded. - **Clearing a property you just added removes its chip.** After adding Estimate (or another property) from the Add menu and setting a value, clearing it again now removes the chip instead of leaving an empty one. - **For contributors: the web app is linted for React hook mistakes.** `make check` now runs ESLint with the Rules of Hooks and effect dependencies (`make web-lint`), and the web tests fail on any network request they did not mock. - **Long lists stay fast.** Moving through a list with `j` and `k` now redraws only the two rows involved instead of every row (in a 2,000-task project, 2 rows per key press instead of 2,000). - **Edits and syncs read your tasks once, not once per panel.** The list, the sidebar counts, the project header, labels, calendars and the task panel now share one live copy of your tasks, so each change or sync reads the stored tasks once instead of about six times with a project open. - **Large lists group and encrypt in linear time.** Grouping a list by project and showing a section's completed tasks no longer slow down sharply with thousands of tasks, rows no longer each open their own lookup when encryption is on, and turning encryption on or off no longer re-reads all your data for every batch of 200 items. - **Commands work the same in every language.** In German, French, Spanish, Italian and Portuguese, the Vim `i` command now puts the caret in the task's title, and moving through the list with `j` and `k` releases a project left focused in the sidebar. - **Viewers of a shared project can no longer skip, detach, schedule or estimate its tasks by keyboard.** Shift+K, Shift+O, `s` and `e` now do nothing on a task you can only view, like the other editing keys, instead of making a change the server then undid. - **Today and the sidebar counts move to the new day at midnight** while the app stays open. - **Confirmations are the app's own dialogs.** Making someone the owner, leaving a project, revoking an invite link, undoing an import and requiring two-factor authentication ask in the app instead of the browser's pop-up. Adding a link to a note (Ctrl/Cmd+K) opens a small dialog that accepts only `https://`, `http://` and `mailto:` addresses, instead of silently dropping anything else. - **The Logbook's "done" count and the estimate field's example are translated.** - **The calendar's hour lines follow the theme**, including high contrast and dark. - **More list actions say why they failed.** Bulk keys, park, duplicate, "this occurrence only", adding a subtask and completing from the panel now show a message when the change cannot be saved. - `SECURITY.md` now says, as the server does, that the audit log takes the client address from the last `X-Forwarded-For` hop (the one your proxy appended), with `OPENTODO_TRUSTED_PROXIES` for proxies in front of it. - **Old repeating events in subscribed calendars.** A daily event that started decades ago (before about 1972) showed no occurrences; it now shows every day of the period. - **Attachments move with a project when it is shared.** Files added to a project's tasks before sharing it stayed visible only to the owner. Sharing now moves them with the tasks, and on upgrade the server moves the files of projects shared earlier; members' devices fetch them on their next sync. ## [0.6.1] - 2026-10-17 OpenTodo 0.6.1 makes plain-http installs work: opened by a home server's or NAS's address (`http://192.168.1.10:8080`), the app no longer shows **Offline** with nothing saved. It adds an **Unraid template**, and fixes encryption turning off part-way, settings pages scrolling away, and two small things in the task panel. ### Upgrade notes - **No migration, no new environment variables, no sync or API change.** Replace the image or the binary and restart. - **Image tags.** The release publishes `ghcr.io/pixari/opentodo:0.6.1` and moves `:0.6`, so installs that follow `:0.6` (Coolify, Unraid) get it from their usual update. ### Added - **Unraid template.** A Community Applications template sets up the container on Unraid: the release image, the Web UI on port 8080, the appdata folder on `/data`, and the optional settings with descriptions; nothing is required. Until it is listed in **Apps**, add it from the template URL. The template is published in its own template repository, [pixari/opentodo-unraid](https://github.com/pixari/opentodo-unraid), from a checked copy in `deploy/unraid/upstream/`. The guide covers appdata permissions (the image ignores `PUID` and `PGID`), https behind a reverse proxy, updates and backups. ([Unraid](docs/self-hosting/unraid.md)) ### Fixed - **Repeat says what it accepts, and the time zone button shows once.** The repeat field takes any interval or pattern, such as `every 3 months` or `every 10 days after completion`, not only the suggestions; it now says so under the empty field, suggests `every 3 months`, and keeps its example rule in English in every language (rules are written in English). In the task panel the time zone button appears beside a due date only when the due has a time, and no longer twice while the When editor is open. ([Recurring tasks and reminders](docs/recurring-and-reminders.md)) - **Plain-http installs on a network address work.** Opened as `http://
:` on a home server or NAS (for example Unraid), the app showed **Offline** and nothing you added was saved, because it used a browser function that only exists over HTTPS. Adding and editing, sync and attachments now work over plain http too; installing the app, push notifications, passkeys and end-to-end encryption still need HTTPS. ([Docker](docs/self-hosting/docker.md)) - **Settings no longer scroll past the end of the page.** On General, Integrations, People and Server, the whole app could be scrolled up and away, leaving an empty page below the last card. Labels that are only read out by screen readers sat outside the scrolling area and stretched the page; they now stay inside it. The layout check fails any screen where the page itself scrolls. - **Turning encryption off no longer stops part-way.** With a task or project note on the account, the re-write to plaintext (and, on a fresh page load, the re-write to ciphertext) failed after its first batch with "Transaction has already completed" when the note code still had to be fetched: the fetch let the browser close the database transaction under it. The code is now loaded without letting the transaction go, and an interrupted run still resumes where it stopped. ([End-to-end encryption](docs/e2ee.md)) ## [0.6.0] - 2026-10-16 OpenTodo 0.6.0 speaks your language and looks back as well as ahead. The interface comes in **English, German, French, Spanish, Italian and Portuguese**. **Statistics** and a guided **weekly review** show what you finished, what slipped and what has no next action, computed on your device, with an optional weekly **digest** by email. **Filter by completion date** (`completed = last week`) and a day-by-day **Completed** logbook answer "what did I do this month?". **Habits** keep a streak and a month history, **custom fields** add typed values to a project's tasks, and **display mode** puts a view full screen on a wall tablet or an e-ink panel. Importers from Todoist, TickTick, Linear and Things keep more of what you had, and operators can run many OpenTodo servers in one process with **host mode**. Clicking a task's circle completes it again. ### Upgrade notes - **Migration `0034`** (weekly digest settings) is applied automatically at startup. Back up `/data` before upgrading: there are no down-migrations. - **No new environment variables are required.** Host mode is off unless `OPENTODO_HOST_URL` is set; `OPENTODO_HOST_TOKEN` (required only with it), `OPENTODO_HOST_OPERATOR_ADDR`, `OPENTODO_HOST_IDLE_MINUTES`, `OPENTODO_HOST_MAX_OPEN` and `OPENTODO_HOST_LIMITS` configure it. A self-hosted server behaves exactly as before. - **Sync** is additive: the entities `field` and `review`, the task fields `habit`, `skipped`, `completed_by` and `fields.`, and the `user_prefs` field `language`. Older clients ignore them; upgrade the server before the devices, as for every new field. - **New routes:** `/api/v1/fields`, `GET`/`PUT /api/v1/me/email-digest`, `GET /api/v1/export/archive` (owner), and `completed_after`/`completed_before` on `GET /api/v1/tasks`. All require authentication; the digest settings write is refused on the demo. - **The installed app downloads the five language catalogs once** (about 100 kB gzipped each) so a language switch works offline; the precache grows from about 2.7 to 4.4 MiB. - **Binary size** is about 27 MB (24 MB in 0.5): mostly the language catalogs. - **Image tags.** The release publishes `ghcr.io/pixari/opentodo:0.6.0` and `:0.6` for `linux/amd64` and `linux/arm64` (`:latest` follows `main`). The Coolify template now follows `:0.6`; if you use `:0.5`, change the tag in **Edit Compose file** and click **Deploy**. ### Added - **The app in your language: English, German, French, Spanish, Italian and Portuguese.** **Settings → General → Language** picks the interface language for every device of the account (or **System**, the browser's first shipped language), and the app switches at once, without a reload, also offline once a language has been loaded on the device. Month and weekday names, the clock and the date order on **System** follow it with the browser's region kept (Swiss German stays Swiss German); quick add and voice capture try it first; reminder notifications (**Snooze 10 min**, **Dismiss**, the due date) are written in it by the service worker; server errors are shown in it by error code; the browser extension follows the browser's language. Tasks, project and label names, filter queries and repeat rules are never translated, and the documentation and the Help guide stay English (Help says so). Each language is a separate chunk of about 100 KB gzipped, loaded only when chosen and precached for offline switching; the main chunk grows by about 6.5 KB gzipped. Sync is additive (`user_prefs.language`); `make check` now runs catalog and untranslated-string checks. ([The app in your language](docs/language.md), [Translating OpenTodo](docs/translating.md)) - **Custom fields per project: text, number, date, choice, link and checkbox values on tasks, filterable, exported, in the API and the assistant tools.** **Fields…** in a project's menu (Shift+F) defines up to 30 typed fields, for editors and above; each task of the project then has a **Fields** group in its panel, rows and board cards show up to three values as badges, and on a wide screen the list offers a **Columns** layout, remembered per project on the device. Values work offline and converge field by field: two people editing different fields of one task both keep their change. Filter with `cost > 100`, `client = "Acme"`, `"Order date" <= friday` or `no client`. A task moved to another project keeps its values, hidden until it comes back; deleting a field hides its values without touching them. The REST API has `/api/v1/fields` and a `fields` object on tasks, the export and own-export import carry both, webhooks deliver the values, and the MCP tools list a project's fields and set values. On end-to-end encrypted accounts field names, options and text values are encrypted. Sync is additive (the `field` entity and the task's `fields.` values); upgrade the server before the devices, as for every new field. ([Custom fields](docs/custom-fields.md), [Filter queries](docs/filters.md#custom-fields), [API](docs/api.md#custom-fields)) - **Statistics and weekly review: on-device numbers, a guided review, an optional weekly digest by email.** **Statistics** (g Shift+S, under More) shows for this week, last week, the last 4 or 12 weeks what you completed (each day of a recurring task once, skipped habit days never), created, completed late and let slip, and what is overdue, with bar charts per day, week, weekday and project; everything is computed on the device from your tasks, offline, and nothing is sent or stored. In shared projects a completion counts for whoever completed it (`completed_by`), the assignee for older tasks. **Review** (g r) walks four steps with live data: get the Inbox clear, look back at the week and what is overdue, give every project a next action or archive it, and look ahead seven days; **Finish review** syncs to your devices and the sidebar marks a review as due after seven days. In **Settings → Notifications** each user can opt in to a weekly digest email on their review day (needs outgoing email; not offered with end-to-end encryption, and turning encryption on switches it off; refused on the demo). Sync is additive (a personal `review` entity); migration `0034` adds the digest settings table; `GET`/`PUT /api/v1/me/email-digest`. ([Weekly review and statistics](docs/review-and-statistics.md), [API](docs/api.md#weekly-digest)) - **Host mode: one process serves many OpenTodo servers.** For operators of a hosted service: with `OPENTODO_HOST_URL=https://example.cloud`, each tenant is an ordinary data directory answering at `.example.cloud`. Tenants open on their first request (running their migrations), close when idle unless a device holds a live stream, and at most `OPENTODO_HOST_MAX_OPEN` stay open. One scheduler runs reminders, backups, Trash retention, webhooks, calendar fetches and weekly digests for tenants with work due only, so thousands of idle tenants cost nothing. Per-tenant limits on storage, changes per hour and in total, requests, import size and members refuse single changes with a named reason while the rest keeps syncing; the app keeps a refused change and shows it as waiting. Outgoing email, single sign-on, backup destinations, attachment limits and the AI switches belong to the operator. A token-authenticated operator interface on its own address creates tenants (empty with a setup link, or from any backup archive), suspends, resumes, deletes and exports them. ([Host mode](docs/self-hosting/host-mode.md)) - **Owners of a hosted server can take their data away.** **Settings → Server → Download your data** saves the whole server as one backup archive that `opentodo restore` reads, so moving to a self-hosted server takes one download. ([Host mode](docs/self-hosting/host-mode.md#owners-take-their-data-away), [API](docs/api.md#hosted-servers)) - **Habits: track a repeating task as a habit with a streak and a month history; skip keeps the chain.** Turn on **Track as a habit** under a task's repeat rule (any rule except `after completion`). Its row shows a streak chip, and the task panel shows the current and best streak and a month grid of done, skipped, missed and upcoming days, navigable with the keyboard. Everything is computed on the device from the series' occurrence records, offline too. Skipping a habit records the day as skipped, so the streak holds; a scheduled day that passes without a record breaks it. Skipped days are not completions: they stay out of Completed and `completed` filters and never count towards `for N times`. The new `habit` filter keyword makes a Habits saved view one word. Sync is additive (the task fields `habit` and `skipped`); upgrade the server before the devices, as for every new field. ([Recurring tasks and reminders](docs/recurring-and-reminders.md#track-a-habit), [Habits reference](docs/recurrence.md#habits)) - **Filter by completion date.** `completed = last week`, `completed >= 7 days ago` or `##Work & completed >= this month` list what you finished; a completion date implies completed tasks. Every date term also takes periods: `this week`, `last week`, `next week`, `this month`, `last month`, `next month`, `2 weeks ago`, `3 months ago`, where `=` means within the period and weeks start on your first day of the week. Saved views keep the period, so "last week" moves with the calendar. Closes #86. ([Filter queries](docs/filters.md#periods)) - **Completed is a logbook.** Completions are grouped by the day you finished them (Today, Yesterday, then the date) and **Show more** reaches older days instead of stopping at the latest 100. ([Views](docs/built-in-views.md)) - **Completion ranges for scripts and assistants.** `GET /api/v1/tasks?completed_after=…&completed_before=…`, the same arguments on the MCP `list_tasks` tool, and `ot list --completed-after … --completed-before …` take a date, an RFC 3339 time or Unix milliseconds. ([API](docs/api.md#tasks-completed-in-a-range), [MCP](docs/mcp.md), [Command-line client](docs/cli.md)) - **Who completed a task.** Tasks gain an additive `completed_by` field (the account id, written with every completion and cleared on reopen, null for public link ticks and older completions); the API and exports return it. Older clients keep syncing. ([Sync protocol](docs/sync.md)) - **Display mode.** Today, Upcoming, a project or a saved view full screen in large type with a clock, for a tablet on the fridge or an e-ink panel: open it with **Display mode** in the view's header or menu, leave it with **Exit** or Esc. Today shows overdue and today's tasks, what was done today ticked below, and the next days as columns; it follows other devices within seconds, keeps working offline with a "Last synced" line, and rolls over at midnight by itself. Options live in the address so a bookmark keeps them: `size=m|l|xl`, `contrast=high`, `motion=none`, `days=1..7` and `eink=1` (high contrast, no motion, a clock without seconds). Tasks can be ticked by touch or with Space only where the account may edit them, so a dedicated viewer account makes a safe shared screen. A display can be the device's start view, and a public link accepts `?display=1` for a panel with no account on it. No server change. ([Display mode](docs/display-mode.md)) - **High-contrast and no-motion presentation** for contributors: `data-contrast="high"` on the root overrides the tokens with pure white or black surfaces and 7:1 text, `data-motion="none"` makes every transition instant; both combine with any theme and accent, and the contrast check measures the override. ([Design system](docs/design-system.md#high-contrast-and-no-motion)) ### Fixed - **Clicking a task's circle completes it again.** Since 0.5.0 the circle turned into a selection checkbox while the pointer was over the row, so a click meant to complete a task ticked it for bulk actions instead. The circle now always completes; selection checkboxes appear only once a task is ticked (Ctrl/⌘+click or Shift+click a row, or x) or in touch Select mode. ([Bulk actions](docs/bulk-actions.md)) - **Imports from TickTick and Todoist keep more of what you had.** TickTick tasks keep the time they were created and completed (the backup writes times as `+0000`, which was not read), and a reminder on an all-day TickTick task fires at the time you set (`PT9H` is 09:00 on the day, not 18:00). A Todoist task with several comments lists them under one "Comments" heading, and an absolute Todoist reminder fires at its time in the task's time zone. Each named format now has a second, richer test file covering recurring tasks, reminders, deadlines, checklists, completed and canceled items and boards. ([Import formats](docs/import.md)) ## [0.5.0] - 2026-10-16 OpenTodo 0.5.0 is a new interface on the same data. The task panel is part of the frame and reads like a summary, the sidebar is shorter, Settings has nine sections with a search field, every keyboard shortcut comes from one registry and ? lists the ones for where you are, and on a phone there is a tab bar, a **+** button and full-screen tasks. Around it: **Process the Inbox** one task at a time, a rich-text **note** at the top of every project, **public links** that share a project's list with anyone, optional **outgoing email** for password resets, verified sign-up and invitations, a **Month** calendar and a project **Timeline**, calendar feeds in both directions, attachments, tasks that wait for other tasks, bulk changes, duplicates and templates, and search and commands on Ctrl+K. Every help page is now short and opens with a screenshot. ### Upgrade notes - **Migrations `0031`, `0032` and `0033`** (calendar feeds, public links, pending sign-ups) are applied automatically at startup. Back up `/data` before upgrading: there are no down-migrations. - **No new environment variables are required.** `OPENTODO_CALENDAR_FEEDS`, `OPENTODO_SMTP_*` and `OPENTODO_ATTACHMENTS_*` are optional; everything they configure is off, or has a default, until you set it or the owner turns it on in Settings. - **Sync** is additive: the project fields `icon`, `description` and `notes_doc`, the task field `blocked_by`, the `attachment` entity and the `user_prefs` fields `sidebar_destinations`, `density` and `first_run`. Older clients ignore them; a 0.4 device keeps syncing. - **Two families of anonymous routes are new:** `GET /api/v1/public/{token}` with `POST …/tick` (public links) and the password-reset request and sign-up verification routes (outgoing email). All are rate limited, re-check their secret on every use, and are refused on the demo. Nothing is reachable without a link or without SMTP configured. - **Settings moved.** The fourteen sections of 0.4 are nine; old addresses such as `/settings/security` and old help ids open the right section at the moved rows. The activity feed is **Updates** at `/updates` (old links redirect). Owner settings for attachments, outgoing email and the audit log live under **Server**. - **Some shortcuts changed.** Skip occurrence is Shift+K, Shift+↓ / Shift+↑ extend a selection, . opens the row's menu; the keys printed in the app return with **Show shortcut hints**. Press ? for the current list. - **Binary size** stays at about 24 MB. (The email templates briefly pulled in `text/template`, whose reflection stops the linker from pruning unused code and had grown the binary to 37 MB; they are plain placeholders again.) - **Image tags.** The release publishes `ghcr.io/pixari/opentodo:0.5.0` and `:0.5` for `linux/amd64` and `linux/arm64` (`:latest` follows `main`). The Coolify template now follows `:0.5`; if you use `:0.4`, change the tag in **Edit Compose file** and click **Deploy**. ### Changed - **A calmer frame.** The task panel is now part of the app's frame: it stays put while the list scrolls, keeps its task in the address (reload or share a link and it reopens), and opens the same way from every list, the calendar, filters and dashboards. The sidebar scrolls as one, with Help and your account always at the bottom and Labels as their own group. Task rows line up across every view and show a second line only when there is something to show. The duration filter lives under a **Filter** button in the list's header, with one chip while it's on; the Inbox no longer labels every row "Inbox". Press ? for a searchable cheat sheet that lists the shortcuts for where you are first. ([Views](docs/views.md), [Keyboard shortcuts](docs/keyboard.md)) - **A task panel that reads like a summary.** The checkbox and title stay at the top while you scroll, with **Expand** opening the task as its own page. Notes are one quiet line until you click them. The properties are chips that read their values, with **Do on** and **Priority** always there and everything else under **Add** (each entry with its shortcut); a chip opens its editor in place, and the date chips share one **When** editor for the due date, start, deadline, evening, repeat and reminders. Subtasks are a checklist with a quiet add line, attachments start as one **Attach** action, the comment box stays at the bottom of the panel, and Activity starts with when the task was created and completed. Completing, deleting, moving, rescheduling or parking a task offers **Undo** in a toast, offline too. ([Tasks and projects](docs/tasks-and-projects.md#edit-a-task)) - For contributors: a page template, a tooltip and skeleton, sidebar building blocks, a style lint over the screens and an interface budget every screen change states ([Interface guide](docs/interface-guide.md)). - **A shorter sidebar and clearer names.** The sidebar starts with Inbox, Today, Upcoming and Calendar; Anytime, Someday, Assigned to me, Completed, Updates, Trash and Filter wait under **More** (the unread count shows on its header while it is closed), and any of them can be promoted next to the primary four from **Settings → Preferences**. **Calendar** is one destination that remembers Day or Week, with its planning list in the panel. The sidebar's "Views" group is now **Filters**, the activity feed is **Updates** at `/updates` (old links redirect), and Settings → You → Activity reads **Sessions and audit log**; the search box still answers to the old words. Every page shares one header with its title, actions, a **⋯** menu and a **Help** button that opens the guide for that screen; projects keep Share and the List/Board switch in it, with Activity and History opening in the panel from the menu, and sub-projects listed as a collapsible group above the tasks. Lists hide empty date groups and an empty "No section", keep a section with only completed tasks as "n completed" with **Show**, and remember which groups you collapsed on each device. Help opens on its search with eight common tasks before the guides, and quick add links to its guide for your first three tasks. ([Views](docs/views.md), [Settings](docs/settings.md)) - **Settings in nine sections.** Settings is one layout: the list of sections (your profile, a **Search settings** field, the groups You, App, Data and Server) is the left column on a wide screen with **Account** already open, and the whole screen on a phone where each section opens as a page with a back link. The sections are **Account** (profile, sessions, password, two-factor, passkeys, linked accounts, encryption), **Appearance**, **General**, **Notifications**, **Capture**, **Integrations** (now with the assistant and AI assistants), **Data** (with archived projects), **People** (users, members, access policies and single sign-on) and **Server** (backups, attachments, the audit log). Old addresses such as `/settings/security` open the right section at the moved rows. Search finds a setting by name or by what it does, in Settings and in the command palette ("passkey" opens Account at the passkeys row). Every card has the same shape: a heading, one sentence, rows with the state as a badge beside the label, and the **Help** button sits once in the page header. **Appearance** gains **Density** (Comfortable or Compact, synced, touch targets kept on phones) and the sidebar switches. On the demo, every section is listed and the rows it refuses are shown disabled under one note. ([Settings](docs/settings.md)) - **One shortcut registry.** Every keyboard shortcut is declared once, with the scope it belongs to (everywhere, a view, a task list, the board, the calendar, the task panel, the notes editor or the Vim layer), and one dispatcher resolves a key in the innermost scope on screen, so a key runs one action and never two. The ? cheat sheet, the keyboard guide and the shortcut column of the command palette all read the registry. Resolved clashes: **Skip occurrence** is now Shift+K (in a list and in the panel) while Shift+S stays **Share project**; s schedules in a list and saves in Filter; . opens the focused row's menu or the Actions menu; a adds a subtask in a list and a card in a board column; Shift+↓ / Shift+↑ extend a ticked range (Shift+J / Shift+K no longer do). Shortcut hints live in tooltips, menus and the palette; the keys printed in the app itself (quick add's q, the sidebar's g keys, prompts) return with **Show shortcut hints** in Settings → General → Keyboard, per device. The palette groups its results under Recent, Tasks, Projects, Labels, Filters, Commands and Settings. ([Keyboard shortcuts](docs/keyboard.md)) - **Built for a phone.** Below 768 px a bar at the bottom of the screen switches between Today, Inbox, Upcoming, Search and More (the sidebar as a drawer), and a round **+** button adds a task from any page as a bottom sheet with the page's defaults and the keyboard up; both step aside while a sheet or a task is open. A task opens as a full-screen page with Back, or a swipe down on its header, to close it. A project's board shows one column at a time with a dot pager and the column's name under the title, and "No section" is a column only while it has open tasks. The Week calendar is a day-by-day list on a phone. The sidebar's project chevrons are full-size touch targets. ([Getting started](docs/getting-started.md), [Installing the app](docs/install-app.md)) - **Shorter help pages.** Every guide in the app's Help is now under 600 words and opens with a screenshot: the long guides (tasks and projects, views, sharing, time blocking, the assistant, settings, notifications, import, trash, troubleshooting and more) became short overview pages that link to one page per topic, 31 new pages in all, and every old address and section link still lands. The docs check enforces the length and the screenshot. ([Help inside the app](docs/getting-started.md#help-inside-the-app)) - **Rows that read at a glance, and a first run.** Every task row is built from the same parts: the date reads "Today · evening · repeats" in one tone, a deadline carries its flag, and the estimate, progress, comments, attachments and project are quiet chips. Completing a task plays a short tick before the row leaves (none under reduced motion), the selection box takes the checkbox's place on hover without shifting the title, and a dragged row lifts over a dotted drop line. Lists, panels and settings show skeletons instead of "Loading…" while local data arrives. The first time an account opens the app, the Inbox shows a three-step card (first task, light or dark, install the app) with **Skip all** and **Show me around**, which creates one sample project of six tasks you can delete like any other; the card never returns on that account, on any device. Every empty state names its action with one button, with the key hint only where there is a keyboard. For contributors: the legacy `btn-*`, `.input` and `.kbd` classes are gone and the style lint covers every screen, the core screens have stories and visual baselines, dark surfaces keep a visible hairline, and `make screens` captures before/after images of the seeded fixture for a pull request. ([Getting started](docs/getting-started.md), [Interface guide](docs/interface-guide.md)) ### Added - **Process the Inbox.** **Process** in the Inbox header (or g p) shows one task at a time and a key for each decision: t today, s schedule, m move, a delegate to someone in a shared project, l labels, y Someday, c complete, Delete trash, j skip, k back. Every decision has Undo, tasks captured meanwhile join the end, and it works offline and with Vim keys. ([Views](docs/views.md#process-the-inbox)) - **Outgoing email (optional).** Point the server at an SMTP server in **Settings → Server → Outgoing email** (or with `OPENTODO_SMTP_*`), check it with **Send test email**, and it can mail what you used to copy by hand. People reset a forgotten password themselves from **Forgot password?** on the sign-in screen. With open sign-up, a new address is confirmed by email before the account exists. Invitations and project invite links have **Send by email**, and the Users panel offers **Send reset email** beside **Create reset link**. Messages come in the six app languages and never carry task content. Nothing changes without SMTP. Migration `0033`. ([Outgoing email](docs/self-hosting/outgoing-email.md)) - **Project notes.** Every project can carry a note at the top, with headings, lists, check lists and links: the goal, the links you keep looking up, a packing list. Click it or press Shift+P to edit it in place with the task-notes editor; two people can edit at once and keep both changes. Owners, admins and editors can edit; viewers read it. Long notes fold per device, history can restore an earlier version, and the API and exports carry it as the project's `description`. ([Project notes](docs/project-notes.md)) - **Public links.** Share a project's list with anyone, no account needed: **Share → Share with a public link** creates a link that is view-only or lets people tick items off, optionally named and with an expiry. The page shows open tasks by section with due dates and priorities, never notes, labels or names, and refreshes itself. Ticks reach every device and read **Public link** in history and activity. A **Public** badge marks published projects; revoking a link stops it at once. Not available for end-to-end encrypted projects or on the demo. Migration `0032`. ([Public links](docs/public-links.md)) - **Repeat and remind from quick add.** Type `every Monday`, `every 2 weeks after completion` or `remind me 30 min before` (also `jeden Montag`, `erinnere mich…` and the other four quick-add languages) and quick add sets the repeat and the reminders, each shown as a chip you can dismiss. A repeating task without a date starts on the rule's first day. The `ot` command line gains `--repeat`, `--remind` and `--natural`. ([Quick add](docs/quick-add.md), [Command-line client](docs/cli.md)) - **A month at a glance, and projects on a timeline.** Calendar now has **Month** next to Day and Week (g m): six weeks of due dates with today marked, "+N more" on busy days and the planned total against your day length. Drag a task to another day, or select it and use the arrows, and it keeps its time; q adds a task on the focused day. Projects get a third layout, **Timeline** (v cycles list, board and timeline): each task is a bar from its start to its deadline, with arrows between tasks that wait for each other. Drag a bar to move its dates, or its ends to set the start and deadline, and use the arrow keys for the same. Both work offline. ([Time blocking](docs/time-blocking.md#see-the-month), [Sections and boards](docs/sections-and-boards.md#see-a-project-on-a-timeline)) - **Calendar feeds, both ways.** Subscribe to the iCal address of your Google, Outlook, Apple, Fastmail or Nextcloud calendars under **Settings → Integrations → Calendar feeds**. Their events then appear read-only in Day and Week, next to your tasks: timed events as blocks in the calendar's colour, all-day events above the unscheduled lane, and free events as an outline. They share the overlap marker but never count toward the day's total. The **Calendars** button hides them on a device, and they stay visible offline. The other way, **Create feed URL** gives you a private address for Google Calendar, Outlook or Apple Calendar that shows your dated tasks, with Rotate and Revoke. The owner turns it on (`OPENTODO_CALENDAR_FEEDS=true` presets it); private network addresses follow the webhooks setting, and the demo refuses it. Migration `0031`. ([Calendar feeds](docs/calendar-feeds.md)) - **Project icons and a sidebar that fits.** Give a project an icon from its menu, next to the colours; it shows in the sidebar, the project header and task rows. Drag the sidebar's edge to make it 15 to 22.5 rem wide, or press [ to collapse it to icons and back (on wide screens). ([Tasks and projects](docs/tasks-and-projects.md)) - **Change several tasks at once.** Tick tasks with x or Space, extend with Shift+↓/↑, tick everything with Ctrl/⌘+A, Shift+click a range, or tap **Select** on a phone. A bar shows how many are ticked; **Actions** (or .) completes, sets priority, labels, a date, moves, assigns or trashes them all in one batch that works offline and syncs as ordinary changes. Moves and assignments are shown for review first; everything else applies at once with **Undo** (also Ctrl/⌘+Z). Works in every task list, the Filter view, saved views and from the search box. ([Tasks and projects](docs/tasks-and-projects.md#change-several-tasks-at-once)) - **Duplicate projects and tasks, and start projects from templates.** **Duplicate…** in a project's menu copies its sections, sub-projects and open tasks, optionally moving every date so the work starts on a day you choose; Shift+D (or **Duplicate** in the task menu) copies a task with its subtasks right below it. **Save as template…** downloads a project as a small file, and **From a template…** (under **+** next to Projects) starts a fresh project from such a file, including the downloads on open-todo.app, with dates counted from your start day. Copies are made on your device and work offline; completed tasks, comments, reminders and assignees stay behind. ([Templates](docs/import-export.md#templates)) - **Tasks that wait for other tasks.** Open a task and choose **Blocked by** (or press b on a row) to pick the tasks it waits for. While any of them is open, the task is dimmed with a lock and a count, in lists and on boards, and Today keeps it in a folded **Blocked** group until it's ready. **Blocks** lists what waits for a task. Completing the last blocker unblocks it on every device, and in a shared project the assignee gets an "is ready to start" notification. Filter with `blocked` and `unblocked`. Calendar apps see the links as `DEPENDS-ON`, and CSV and Linear imports bring them along. Two people can't make tasks wait for each other, even offline. ([Tasks and projects](docs/tasks-and-projects.md#wait-for-another-task)) - **Attachments on tasks and comments.** Add files and links to a task from the new **Attachments** row in the task panel (pick, paste a screenshot or drop files), or attach files while writing a comment. Files added offline are listed at once and upload on the next sync; every device sees them as soon as the server has stored them. Images get thumbnails, task rows show a paperclip with the count, and the installed app takes photos and documents from the phone's share sheet. The browser extension can add a screenshot of the page. Backups and the new **Download export with files (ZIP)** include the files, and importing that ZIP brings them back. Encrypted accounts encrypt the files on the device too. The owner sets a size limit per file and a total per account under **Settings → Administration → Attachments**, and can turn attachments off. ([Attachments](docs/attachments.md)) - **Search and commands on Ctrl+K.** One box (Ctrl/⌘+K, or the magnifying glass in the header) finds any task by title or notes, any project, label, saved view or Settings section, and runs any command by name: go to a view, act on the selected task, use everything in the current project's, view's, label's or section's menu, create things, sync, toggle Vim keys. Filter queries like `@errand & p1` list their tasks right there. It ranks what you pick often first, works offline, and searches only on your device, also for encrypted accounts. ([Search and commands](docs/search.md)) ## [0.4.0] - 2026-10-15 OpenTodo 0.4.0 makes the app yours to look at and easier to move into. Tasks can have a deadline, separate from the day you plan to work on them, and can wait for a start date, in Someday or for This Evening. Pick light, dark or system mode and one of eight colour themes, and set your own date format, clock and first day of the week. Settings are regrouped into clear sections. Import takes Todoist, Things, TickTick and Linear exports as they are, with a switch guide for each. With the assistant on, an **Ask** box and built-in skills (plan my week, triage my inbox, break this down) do common jobs. A setup token can protect a fresh public server, and the assistant's **Test connection** now checks what is on screen. ### Upgrade notes - **Migration `0030`** (how often each assistant skill ran, per user and month; no conversation content) is applied automatically at startup. Back up `/data` before upgrading: there are no down-migrations. No new environment variables are required; `OPENTODO_SETUP_TOKEN` is optional. - **Sync:** new `user_prefs` fields (`date_format`, `time_format`, `week_start`, `theme_mode`, `theme_accent`), the task fields `deadline`, `start` and `evening`, and the reminder kind `deadline` are additive; older clients ignore them. - **Image tags.** The release publishes `ghcr.io/pixari/opentodo:0.4.0` and `:0.4` for `linux/amd64` and `linux/arm64` (`:latest` follows `main`). The Coolify template now follows `:0.4`; if you use `:0.3`, change the tag in **Edit Compose file** and click **Deploy**. - **Binary size** stays at about 23 MB. ### Breaking - **`POST /api/v1/me/assistant/test` no longer answers `409 no_own_provider`.** A member can now test a provider before saving it; with no provider chosen or saved, the route answers `400 invalid_input`. **Migration:** scripts that checked for `no_own_provider` should check for `invalid_input`. ### Changed - **New addresses.** The website and the documentation moved to [open-todo.app](https://open-todo.app) (docs at [open-todo.app/docs](https://open-todo.app/docs/)) and the public demo to [demo.open-todo.app](https://demo.open-todo.app). In-app **Help** links, the generated docs' canonical URLs and the Coolify template now point there. - **Dates and times in your format.** **Settings → Preferences → Date and time** sets the date format (day-month, month-day or ISO), a 12- or 24-hour clock and the first day of the week, once for all your devices; each follows the browser until you choose. Task rows, headings, task details, reminders, comments, history, the Week view and push notifications follow it. Due dates and reminders get new date and time fields that accept what you type in your format (`3/11`, `2026-11-03`, `tomorrow`, `3pm`) and have a calendar you can use from the keyboard. Quick add reads numeric dates in your date format while its own order is automatic, and "next week" starts on your first day of the week. The preferences sync like your other data, work offline and are included in the JSON export. ([Settings](docs/settings.md#date-and-time), [Tasks and projects](docs/tasks-and-projects.md#dates-and-times)) - **Settings: grouped into fourteen sections with a clearer layout.** Settings opens on your profile card and the sections grouped under **You**, **App**, **Data and connections** and, for the owner and admins, **Server**. Security, Encryption, Activity, Preferences, Projects, Backups and Access policies now have their own pages. Every panel is a card that leads with its state (for example whether two-factor authentication is on and how many recovery codes are left), with settings as rows, forms that open from a button, and confirmations that say what will be lost. Old links such as `/settings/general` still work. ([Settings](docs/settings.md)) ### Added - **Start dates, Someday and This Evening.** A task's new **When** row (and Shift+N) defers it until a start date: it leaves Today, Upcoming and the counts, waits in its project's **Later** group, and appears in Today with a **Started** chip on the day. **Someday** (n, or `someday` in quick add) parks a task in the new Someday view (g then o) and clears its due date. **This Evening** (Shift+E, or `tonight` in quick add) lists a task below the day's tasks in Today. The new **Anytime** view (g then n) lists what has no date at all. A due date or deadline that has come always shows a task in Today. Filters gain `someday`, `deferred`, `evening`, `no start` and `start `; recurring tasks move the start date with the due date; CalDAV carries both as `X-OPENTODO-START` and `X-OPENTODO-EVENING`; Things and CSV imports keep them. Sync: two new task fields, `start` and `evening`. ([Tasks and projects](docs/tasks-and-projects.md#when-a-task-starts), [Views](docs/views.md)) - **Deadlines.** A task can have a deadline, the day it must be finished by, separate from the day you plan to work on it. Set it in the task panel's new **Deadline** row, with d on a selected task, or in quick add with `deadline friday` (`Frist`, `échéance`, `scadenza`, `fecha límite`, `prazo` in the other languages). Rows show a countdown (`3d`, `Today`, `2d over`) that turns amber in the last week and red on the day, after it, or when the task is planned after its deadline. Today lists tasks whose deadline is today or past; Upcoming lists undated tasks under their deadline day. Filters gain `deadline <= 3 days` and `no deadline`, reminders can fire before the deadline, recurring tasks move the deadline with the due date, CalDAV serves it as `DUE` (with the due date as `DTSTART`), and imports from Todoist, Things, CSV and JSON keep it. Sync: one new task field, `deadline` (`YYYY-MM-DD`), and a reminder kind `deadline`. ([Tasks and projects](docs/tasks-and-projects.md#deadlines), [Filters](docs/filters.md), [Reminders](docs/reminders.md)) - **Themes and dark mode on demand.** **Settings → Preferences → Appearance** sets the mode (**System**, **Light** or **Dark**) and one of eight colour themes (Emerald, Blue, Indigo, Violet, Rose, Orange, Teal, Graphite) for buttons, links, the focus ring and selection. Both follow your account to every device, also offline, apply at once without a reload, and the app opens in your theme without a flash. Priority, warning and error colours and your project and label colours keep their meaning in every theme, and every theme passes the contrast checks in light and dark. The installed app's title bar follows the theme. Sync: two new `user_prefs` fields, `theme_mode` and `theme_accent`. ([Settings](docs/settings.md#appearance)) - **Assistant skills and the Ask box.** With the assistant on, **Today** and a new **Ask** screen (sidebar, or g then k) show an **Ask OpenTodo** box with example cards, and the assistant panel gains a **Skills** menu: *Turn an idea into a project*, *Plan my week* (within your working day), *Weekly review*, *Triage my inbox* and *Break this down* (also in a task's menu). Each skill uses only the tools it needs, and everything it proposes is reviewed and undoable as before. Pasted notes are kept apart and treated as material, never as instructions. The same skills are served as MCP prompts. While the assistant is off, members see nothing new and the owner one dismissible line. New route `GET /api/v1/assistant/skills`; turns accept an optional `skill` and `workday`. ([AI assistant](docs/assistant.md#skills), [MCP](docs/mcp.md#prompts)) - **Switch from Todoist, Things, TickTick or Linear.** Import accepts each app's own export as it is: a Todoist backup ZIP (or one project's CSV), a copy of the Things 3 database, a TickTick CSV backup and a Linear CSV export. Projects, sections or headings, subtasks and checklists, labels, priorities, due dates (Todoist's in six languages, even without a year), recurring tasks, reminders and completed items carry over; projects that were boards open as boards; Linear issues assigned to you stay yours. What OpenTodo cannot hold yet (comments' authors, estimates in points) is named in the preview. Each app has a switch guide. ([Switch from Todoist](docs/switch/todoist.md), [Things](docs/switch/things.md), [TickTick](docs/switch/ticktick.md), [Linear](docs/switch/linear.md), [Import formats](docs/import.md)) - **Site bundle for the website.** Every release attaches `opentodo-site-bundle-.tar.gz`: the documentation website, product screenshots, the colour tokens as plain CSS, the icon set as an SVG sprite, the changelog as JSON and a manifest (schema 1) with the version, the import formats it supports and a few measured facts. Every CI run uploads the same bundle as an artefact, so the website can preview unreleased docs. Layout version 1 is described in the bundle's `README.md`; paths change only in a release that says so, and stay for one more release. `make site-bundle` builds it locally. Screenshots for the website now live in `docs/screenshots/site/`, and CI skips the Go, cross-compile and layout jobs for documentation-only pull requests. - **Server facts in the site bundle.** The bundle manifest now lists the image platforms, port, data folder, database, licence and export format (`architectures`, `port`, `dataDir`, `database`, `license`, `exportFormat`, `exportFormatVersion`), read from the release workflow, the Dockerfile, the export route and LICENSE, so the website can show them for the version it describes. Its icon sprite also carries the Lucide icons the website uses beyond the app's own (`web/src/ui/icons/site-icons.json`), which the app itself never loads. - **A setup token protects a fresh server.** With `OPENTODO_SETUP_TOKEN` set, creating the first account requires that token, so a public instance cannot be claimed by whoever opens the URL first. The Coolify template sets it to a secret Coolify generates; copy it from the service's Environment Variables into the setup screen. Unset, nothing changes. ([Coolify](docs/self-hosting/coolify.md#first-visit), [Docker](docs/self-hosting/docker.md#docker-compose)) - **`ot mcp`: MCP for assistants that only speak stdio.** The command-line client bridges an assistant's stdio MCP connection to your server's `/mcp` endpoint, so clients without HTTP transport can use OpenTodo too. It refuses a token that can make changes unless you pass `--allow-full-token`, and never weakens `--read-only` or `OPENTODO_READ_ONLY`. Thanks to @adity982 for the contribution. ([AI assistants (MCP)](docs/mcp.md)) - **Icons, menus and a smart picker.** Every emoji and symbol that stood in for an icon is now a line icon from one consistent set (Lucide, bundled, no new download). Menus have icons, groups and shortcut hints and work fully with the keyboard (arrow keys, Home and End, typing a letter). The label and assignee pickers show the labels and people you use most on this device first, and find them by the start of any word or by letters in order, ignoring case and accents (“cafe” finds “Café”). Your pick history stays on the device and is cleared when you sign out. ([Design system](docs/design-system.md#building-blocks)) - **New visual foundation.** Design tokens for colour, type, spacing, radius, elevation and motion in light and dark, the bundled Inter typeface (54 KB, served by your own server, never a font CDN), and refreshed buttons and inputs, keeping OpenTodo's emerald and slate look. Every text colour meets WCAG AA contrast in both themes, checked automatically. Contributors get a component workbench (Storybook, development only) with accessibility and visual-regression checks. ([Design system](docs/design-system.md)) ### Fixed - **Test connection tests what is on screen.** In the assistant settings, **Test connection** used the last saved settings, so a model id typed but not yet saved was reported as "enter a model id first", and a replaced key was never checked. The owner's and the member's tests now use the provider, model, base URL, key and private-endpoint switch on screen, saved or not, with the same checks as saving, and never save anything. Members can test before saving their own provider. A result disappears when you change what it tested. ([AI assistant](docs/assistant.md), [API](docs/api.md)) - **The assistant says what an encrypted account can do.** On an account with end-to-end encryption, the assistant panel, the Ask screen and **Settings → AI → Assistant** now link to **Settings → Encryption**, where encryption can be turned off to use the assistant, instead of only saying it is unavailable. ([AI assistant](docs/assistant.md)) - **Test notifications prove delivery.** **Settings → Notifications → Send test notification** used to show a notification from the page and always say "sent", even when reminders could never reach the device. It now sends a real push through your server and the push service, shows it even while the app is open, and says **Received on this device** only when the device confirms it, otherwise **accepted, not seen** with the operating-system settings to check. A new **Delivery check** lists each step push needs on the device (permission, service worker, subscription, push service, last test) with a fix for each problem. New route `POST /api/v1/push/test` (session or full token, 5 per 10 minutes). ([Notifications](docs/notifications.md#check-that-it-works), [API](docs/api.md#reminders-and-push)) ## [0.3.1-rc.1] - 2026-10-15 A release candidate that tests the release pipeline: the release now attaches `opentodo-site-bundle-v0.3.1-rc.1.tar.gz` (the documentation website, screenshots, colour tokens, icons, changelog and feature manifest for the website) next to the binaries, listed in `SHA256SUMS`. It carries everything under Unreleased above; it is not a supported release, and the `:0.3` and `:latest` image tags do not move. ## [0.3.0] - 2026-10-12 OpenTodo 0.3.0 is about bringing your own AI, if you want one. There is a chat assistant inside the app that answers questions about your tasks and proposes changes you review and undo in one step, with Anthropic, OpenAI, Gemini or a model on your own network. An assistant you already use can now make changes over MCP, and the `ot` command is ready to be driven by coding agents. All of it is off until the owner turns it on; nothing is sent anywhere before that. [Use AI with OpenTodo](docs/ai-and-agents.md) compares the three options. Settings is also split into sections with their own pages, and a project can be shared with a single invite link. ### Upgrade notes - **Migrations `0026` to `0029`** (agent tokens and switches; assistant consent and usage; members' own assistant provider; project invite links) are applied automatically at startup. Back up `/data` before upgrading: there are no down-migrations. No new environment variables are required; `OPENTODO_MCP_WRITES` is optional. - **Image tags.** The release publishes `ghcr.io/pixari/opentodo:0.3.0` and `:0.3` for `linux/amd64` and `linux/arm64` (`:latest` follows `main`). The Coolify template now follows `:0.3`; if you use `:0.2`, change the tag in **Edit Compose file** and click **Deploy**. - **`ot` scripts:** destructive commands now need `--yes` off a terminal, and refusals exit 5 instead of 1 (see Breaking below). - **Binary size.** The server binary grows from about 17 MB to about 23 MB, mostly the official Anthropic SDK used by the assistant. ### Breaking - **`ot`: destructive commands need `--yes` without a terminal.** `ot rm`, `ot project rm`, `ot token revoke` and `ot auth logout` used to proceed silently when stdout was not a terminal or `--json` was given. They now refuse with exit code 2 (`confirmation_required`) before sending anything. **Migration:** add `--yes` to scripts and cron jobs that delete on purpose, e.g. `ot rm "$ID" --yes`. On a terminal without `--json` they still ask. ([CLI](docs/cli.md#migrating-scripts)) - **`ot`: new exit code 5, "not permitted".** Refusals for scope, role, a switch, encryption or read-only mode (`insufficient_scope`, `forbidden`, `not_permitted`, `writes_disabled_by_owner`, `writes_disabled_by_user`, `encryption_enabled`, `read_only`) exit 5 instead of 1, so scripts and agents can stop instead of retrying. **Migration:** scripts that test for exactly `1` should also accept `5`; scripts that test for non-zero are unaffected. The op log and the REST API are unchanged. ### Added - **Invite to a project with a link.** In a project's **Share** dialog, the owner and admins can create a single-use **invite link** with a role (viewer or editor; admin for the owner), an optional email binding and an expiry from 1 hour to 30 days (default 7 days). The link is shown once; pending links are listed with their role, email, expiry and creator and can be revoked. Opening `/invite/p/` shows who invited you to which project: signed in, you select **Join project**; otherwise you sign in and join, or create an account that joins in the same step. A link creates accounts only when its creator is the server owner or an admin, when open sign-up is on, or when the owner switches on **Members can invite new people with project links** in **Settings → Administration** (off by default); otherwise the page asks the person to get an account first. Links stop working when revoked, used or expired, when the project is deleted, or when their creator is no longer the project's owner or an admin. Links are server-side data, never synced; refused on the demo instance. ([Sharing](docs/sharing.md#invite-with-a-link), [API](docs/api.md#project-invite-links)) - New routes: `GET/POST /api/v1/projects/{id}/invites`, `DELETE /api/v1/projects/{id}/invites/{inviteId}`, `GET /api/v1/project-invites/{token}` (public preview), `POST /api/v1/project-invites/{token}/accept`, `GET/PUT /api/v1/settings/project-invites`; `POST /api/v1/auth/register` accepts `projectInvite`. New error code `project_invite_not_allowed`; audit events `project_invite.created`, `project_invite.revoked`, `project_invite.accepted`. Migration `0029_project_invites.sql` is applied automatically. - **Your own assistant provider.** When the owner lets members bring their own key, each person can choose Anthropic, OpenAI or Google Gemini, a model and their own key in **Settings → AI → Assistant → Which assistant you use**, with **Test connection**. Members are never offered the compatible endpoint, so they cannot make the server connect to an address of their choosing. The disclosure names the provider actually used ("from this server to … with your own API key") and is asked again when it changes. Turning member keys off makes everyone use the owner's provider again; choices are kept for later. Keys stay in `/secrets/assistant.json`, never returned, logged, exported or backed up. ([AI assistant](docs/assistant.md#use-your-own-provider-and-key)) - **Own keys outside the allowance.** Answers on a member's own key are recorded and shown as "your key", but neither counted nor stopped by the monthly token allowance unless the owner turns on the new **Count members' own keys toward the monthly limit** (`cap_member_keys`). One answer per person and 8 per server still apply. The owner sees how many people use their own provider and key. - API (additive): `own_provider`, `own_model` and new status fields on `GET/PUT /api/v1/me/assistant`; `POST /api/v1/me/assistant/test` (signed-in sessions, 5 per 10 minutes); `cap_member_keys` and `own_key_members` on `/api/v1/settings/assistant`; `counted_tokens` and `own_key` on the usage route and `own_key` on the turn's `usage` event. ([API](docs/api.md#in-app-assistant)) - **AI assistant in the app.** A chat panel (Shift+I or **Assistant** in the header; a side sheet on wide screens, full screen on phones) that answers questions about your tasks and *proposes* changes: "move everything overdue to Friday", "break this task into steps". Each proposal is a reviewable list with every change in words; you apply the checked part (at most 50 changes) and undo it in one step. Applied changes are ordinary edits from your device: offline-capable, synced, labelled **Assistant** in history. Providers, each with the owner's or a member's own key: Anthropic through the official Anthropic Go SDK (default `claude-opus-5-5`; `claude-sonnet-5-5` and `claude-haiku-5-5` offered), OpenAI, Google Gemini and any OpenAI-compatible endpoint, including Ollama or LM Studio on your network. Off by default: the owner enables it in **Settings → Assistant** (provider, models, keys, private endpoints, monthly token allowance, member keys, **Test connection**); each person accepts a disclosure naming the provider and the fields sent before their first message, and again when the provider changes. Keys live only in `/secrets/assistant.json`, never returned, logged, exported or backed up. The assistant cannot change sharing, members, settings or tokens, make outbound requests or delete permanently; task text is marked as untrusted. Conversations stay on the device and are deleted at sign-out. Not available for end-to-end encrypted accounts or on the demo instance. Optional per-device auto-apply of up to 3 small, reversible changes, off by default. ([AI assistant](docs/assistant.md), [API](docs/api.md#in-app-assistant), [security](SECURITY.md#in-app-assistant)) - New routes: `GET/PUT /api/v1/settings/assistant`, `POST /api/v1/settings/assistant/test` (owner), `GET/PUT /api/v1/me/assistant`, `GET /api/v1/me/assistant/usage`, and `POST /api/v1/assistant/turn` (signed-in sessions only; streams server-sent events). `/api/v1/sync` accepts the op device prefix `assistant:`; history entries from it carry `device_label: "Assistant"`. - Dependency: `github.com/anthropics/anthropic-sdk-go` (the server binary grows by about 5 MB; see the change's design notes). - **`ot` for agents and automation** ([CLI](docs/cli.md#using-ot-from-an-agent)): `--json` output is documented with JSON Schemas and `output_version` 1 (`ot version --json`); `ot schema` (or `ot help --json`) prints every command, flag, exit code, error code and output schema, offline; `--dry-run` on every write command resolves references and prints the requests it would send without writing; `--idempotency-key` on `ot add` and `ot project add` makes retries return the same entity; `ot done`/`ot reopen` on a task already in that state report `unchanged` and send nothing, and `ot edit` with unchanged values sends nothing; `--read-only` (or the optional `OPENTODO_READ_ONLY=1`) refuses every write before any request; `ot` refuses writes locally for `read` tokens and outside the allowed routes for `agent` tokens; `ot auth status` shows the token's scope and whether assistant writes are allowed. With `--json`, errors carry an additive `exit` field and stderr gets one `ot:` line; `done`/`reopen`/`rm` add `ref` and `status` to each task. - **Agent skill file.** `ot skill` prints `SKILL.md`, which teaches an agent to use `ot` safely (agent token, `--json`, dry runs, idempotency keys, exit codes, task text as untrusted data). Each release ships it as `opentodo-agent-skill.zip`. - **`Idempotency-Key` on `POST /api/v1/{collection}`**: the entity and op ids derive from your account, the collection and the key, so a retried create returns 200 with the stored entity and `Idempotent-Replayed: true`; the same key with other content answers 409 `idempotency_conflict`. Keys never cross accounts and need no storage. ([API](docs/api.md#idempotency-keys)) - **`agent` tokens on REST content routes**: creating tasks, projects, labels and comments, editing tasks, renaming or recolouring projects and trashing the token's own tasks, under the same two switches, limits, `agent.write` audit, history label and encryption rules as the MCP write tools (they run through the same tool registry). Every other REST write still refuses them. ([API](docs/api.md#agent-tokens)) - `GET /api/v1/me` gains an additive `auth` object: `method`, `scope`, `token_id`, `token_name` and, for `agent` tokens, `writes_allowed`/`writes_denied`. Never the secret. ([API](docs/api.md#credential-introspection)) - **AI assistants can make changes, when you allow it.** The MCP endpoint gains ten write tools: `create_tasks`, `update_tasks`, `complete_tasks`, `reopen_tasks`, `move_tasks`, `schedule_tasks`, `create_project`, `create_label`, `add_comment` and `trash_tasks`. They appear only when the owner turns on **AI assistants may make changes**, you turn on **Allow AI assistants to make changes to my tasks** (both off by default, in Settings → AI assistants), and the assistant uses a new **`agent`** token. Full-access and read-only tokens keep read-only tools. Every change goes through the op log, syncs and converges like any edit, shows in history as "AI assistant (*token name*)", is recorded in the audit log as `agent.write` (no content), and can be undone. Nothing is ever deleted permanently; `trash_tasks` only accepts tasks the same token created; assistant comments notify no one. Limits: 50 items per call, 30 write calls per minute and 1,000 changed items per day per token. For end-to-end encrypted accounts, tools that would write readable text are not offered (`encryption_enabled`); scheduling, completing and moving still work. ([AI assistants](docs/mcp.md#letting-an-assistant-make-changes)) - `agent` token scope: refused on every REST write like `read`, except the agent content routes above (it cannot create tokens, change settings or sync raw operations). Create one in Settings → AI assistants, Settings → API tokens, or with `ot token create --scope agent`; `ot token list` shows each token's scope. - `GET/PUT /api/v1/me/agent-access` (your own switch; sessions and full tokens only) and `writes_enabled` on `/api/v1/settings/mcp`. The optional `OPENTODO_MCP_WRITES=true` presets the owner's switch on first start. - New internal package `internal/agenttools`: one registry of agent tools (definitions, validation, planning, limits, permission gate), to be shared with the planned in-app assistant. - **Coolify one-click template, ready to submit upstream.** `deploy/coolify/opentodo.yaml` now follows Coolify's current template conventions: `SERVICE_URL_OPENTODO_8080`, the `0.2` image line, a `/data` volume and a `/opentodo healthcheck` health check. It also lists the useful optional variables with their defaults (including `OPENTODO_TRUSTED_PROXIES` and email to task), because Coolify passes a variable from its UI only when the compose file references it. An explicit `OPENTODO_BASE_URL` wins over Coolify's generated URL. `deploy/coolify/upstream/` mirrors Coolify's repository paths and holds a draft docs page, the pull request text and step-by-step submission notes. Until the template is listed in Coolify, paste it as a **Docker Compose** resource. ([Coolify](docs/self-hosting/coolify.md#one-click-template-pending-in-coolifys-service-list)) - `scripts/coolify-template-check.mjs` (in `make check` and CI) fails when the template breaks those conventions, names an `OPENTODO_*` variable the server does not read, lags behind the latest release, or differs from its upstream copy. `make coolify-smoke` starts the image the way Coolify does and checks the health check, the owner setup, closed sign-up, the https base URL and data surviving a redeploy. ### Changed - **An invalid `OPENTODO_TRUSTED_PROXIES` entry no longer stops the server.** A typo such as `cloudflar` used to fail startup, so the health check failed and Coolify rolled the deploy back with the cause only in the container log. Now the server starts, ignores the invalid entries (keeping the valid ones; with none valid it behaves as unset, so trust is never widened), logs one error line naming them with a "did you mean cloudflare?" hint, and shows the owner a **Server configuration warning** at the top of **Settings** and **Settings → Administration**. Entries may also be separated by spaces. `GET /api/v1/me` gains an additive `config_warnings` array for the owner's session only. Every other invalid variable still stops startup. ([Behind a reverse proxy](docs/self-hosting/docker.md#behind-a-reverse-proxy)) - **Settings has sections.** Instead of one long page, `/settings` lists the sections with a line on what each holds, and each opens on its own page: **Account**, **General**, **Notifications**, **Data**, **Capture**, **Integrations**, **AI** and, for the owner and admins, **Administration** (`/settings/account`, `/settings/general`, …). Wide screens keep the section list beside the content; phones get a back link. Every setting works as before, Help links open the same guides, and `/settings/import` is unchanged. The user guide now names the section, for example **Settings → AI → AI assistants**. The single sign-on link flow returns to `/settings/account`. ([Settings](docs/settings.md)) - MCP read results mark text people wrote: titles, descriptions, names and comment bodies now sit inside an `untrusted_text` object, every result carries a `_notice` telling the assistant to treat that text as data, tasks of shared projects carry `shared: true` and comments `author_is_you`. Label ids are listed as `label_ids` next to the names. The tool definitions themselves are unchanged. ([AI assistants](docs/mcp.md#read-tools)) - `/api/v1/sync` refuses operations from devices whose id starts with `agent:` or `caldav:` (400 `invalid_op`): those ids are reserved for the server's own writers. - API requests with a token scope this server does not know are refused on every write route (previously only `read` was). - **Migration `0026`** adds two side tables (`api_token_agent`, `user_agent_access`) and is applied automatically at startup. Rolling back to an older version is safe: `agent` tokens then act as read-only tokens. - Images built from source by Coolify report `dev+` instead of `dev` when the application's **Source commit availability** is *Available during build* (the Dockerfile reads `SOURCE_COMMIT`). - Coolify guide: which resource types roll and which recreate, why plain **Restart** and API deploys keep a cached image, first-visit owner setup, domains and passkeys, Cloudflare (proxy and Tunnel, Browser Cache TTL), and Coolify volume backups. ### Fixed - A reminder that became due while a dialog, menu or popover was open took the keyboard focus out of it. It is still announced, but focus now stays where you are. - Phones could keep running the previous release after an update when a CDN such as Cloudflare sat in front: it cached the service worker script for hours. Service worker scripts are now sent with `Cache-Control: no-store` (and `CDN-Cache-Control: no-store`). ## [0.2.0] - 2026-10-11 OpenTodo 0.2.0 is about trying it, using it on a phone and running it without fuss. Every visitor to the public demo now gets a private sandbox. The phone layout has been reworked so menus, pickers and dialogs fit small screens. There is a docs website and a Help view inside the app, which works offline. Archived projects can be restored with one click. Updates roll out without a Stop → Deploy. Rate limits work correctly behind trusted proxies and Cloudflare. The app meets WCAG AA contrast in light and dark, and this release includes several security fixes. ### Upgrade notes - **From 0.1.0, stop once.** 0.1.0 locked the data directory exclusively, so a 0.2.0 container cannot start beside it. Plain `docker compose pull && docker compose up -d` works as before. With Coolify or another orchestrator that starts the new container first, click **Stop**, then **Deploy** for this one upgrade (otherwise the deploy fails with "data directory is in use by another opentodo process" and rolls back). From 0.2.0 on, updates roll without downtime. ([Docker](docs/self-hosting/docker.md#updating), [Coolify](docs/self-hosting/coolify.md#updating)) - **Image tags.** The release publishes `ghcr.io/pixari/opentodo:0.2.0` and `:0.2` for `linux/amd64` and `linux/arm64` (`:latest` follows `main`). The Coolify template in `deploy/coolify/opentodo.yaml` now follows `:0.2`; if you pinned `:0.1`, change it. - **Migration `0025`** (demo sandbox bookkeeping) is applied automatically at startup. Back up `/data` before upgrading: there are no down-migrations. No new mandatory environment variables; `OPENTODO_TRUSTED_PROXIES`, `OPENTODO_DEMO_MAX_SANDBOXES` and `OPENTODO_DEMO_MAX_DB_MB` are optional. - **Behind a CDN or second proxy**, set `OPENTODO_TRUSTED_PROXIES` (for example `cloudflare`) so rate limits and the audit log see visitors' addresses. ([Reverse proxy](docs/self-hosting/docker.md#behind-a-reverse-proxy)) ### Added - **User documentation**: seventeen task-based guides for people using the app (getting started, installing the app on iOS, Android and desktop, tasks and projects, views, sections and boards, labels and filters, recurring tasks and reminders, time blocking, sharing and comments, notifications, offline and sync, keyboard shortcuts, account security, import and export, trash and history, settings, troubleshooting and FAQ), with light and dark screenshots generated from the demo data (`make docs-screenshots`). Every page in `docs/` has front matter (title, summary, audience, the capabilities it covers, the app screens it explains). ([Docs](docs/README.md)) - **Docs website** at [opentodo.pixari.dev/docs](https://opentodo.pixari.dev/docs/), generated from `docs/` by `make docs` into `website/docs/`: grouped by audience, in-browser search over a prebuilt index (nothing typed leaves the browser), light and dark, readable without JavaScript, "On this page", previous and next, the version described and "Edit this page on GitHub". "Docs" is in the website's navigation. - **Help inside the app**: a Help view with the user guide of the running version, searchable and available offline (a lazily loaded chunk, precached by the service worker; screenshots stay on the website). Open it from the sidebar, Settings, the `?` overlay, `g` then `?`, or `:help ` with Vim keybindings. Settings sections, the share dialog, the Filter view, quick add and the import screen have a Help link that opens the matching section in a side panel (a full-screen sheet on phones) without closing what you were doing. - **Unarchive projects**: Settings → Projects lists your archived projects, with their parent and open task count, and a **Restore** button that brings one back to the sidebar. Restoring is an ordinary change, so it works offline and syncs to every device; sub-projects stay archived until restored on their own, a project restored by hand is not archived automatically again on that device, and in shared projects only the owner or an admin can restore. Previously only the automatic-archive notice could undo an archive. ([Trash and history](docs/trash-and-history.md#bring-back-an-archived-project)) - `npm run docs:check` (part of `make check` and CI) fails on missing front matter, broken links or anchors, images without alt text, capabilities in `openspec/specs/` that no page covers, help links to unknown sections, a keyboard guide that differs from the app, and stale generated files. The shortcut table moved to `web/src/lib/shortcuts.ts`, shared by the `?` overlay and the keyboard guide. The CI layout job also runs axe-core on the Help view, the help panel and the docs website, and checks the docs website at 360, 390 and 1280 px with and without JavaScript. ### Changed - The public demo (`OPENTODO_DEMO=true`) gives every visitor a **private sandbox** instead of one shared account: "Try the demo" creates a password-less account with its own copy of the sample data and a non-interactive collaborator, and nobody sees anybody else's changes. Sandboxes are deleted after 2 hours of inactivity, 24 hours after creation and by the nightly reset, which now also runs `VACUUM`. The published demo password is gone; the first start deletes the old shared demo accounts. `auth/status.demo` carries a creation token instead of credentials; new route `POST /api/v1/demo/sandbox`, new error codes `demo_expired`, `demo_limit`, `demo_busy` and `demo_token`; optional `OPENTODO_DEMO_MAX_SANDBOXES` and `OPENTODO_DEMO_MAX_DB_MB`. Migration `0025` adds the sandbox bookkeeping tables. ([Public demo](docs/self-hosting/demo.md)) - Links in comments and notes render with `rel="nofollow ugc noopener noreferrer"` on every instance. ### Security - Demo instances bound abuse: sandbox creation needs a single-use, signed, 10-minute token from the sign-in page and a same-origin request, at most 5 new sandboxes per address per hour (addresses are stored only as salted hashes), a live cap with idle eviction and a database size guard; each sandbox may write 600 ops per hour, 5,000 ops and 2 MB of values in total, values are capped at 4 KB (32 KB for notes), and above the database size limit live sandboxes cannot write either. Every demo response carries `X-Robots-Tag: noindex, nofollow` and the demo page declares `noindex`. - Per-address rate limits (sign-in, passkeys, two-factor, single sign-on, CalDAV, demo sandboxes) and audit-log addresses now use the **last** `X-Forwarded-For` hop, the one the reverse proxy appended, instead of the first entry, which the client controls behind an appending proxy such as Traefik and could change to evade the limits. Run one proxy in front of the server and do not publish its port. ([Reverse proxy](docs/self-hosting/docker.md#behind-a-reverse-proxy)) - Behind a CDN, set the optional `OPENTODO_TRUSTED_PROXIES` (IP addresses, CIDR prefixes and/or `cloudflare`) so the CDN's hop is skipped and visitors are not all counted as the CDN's address. With `cloudflare`, requests from a Cloudflare edge use `CF-Connecting-IP`. ([Reverse proxy](docs/self-hosting/docker.md#behind-a-reverse-proxy)) ### Fixed - Phones: every menu, popover, picker and dialog now fits the screen. The notifications panel was cut off on the left; it, the mention picker and the section, project, label and view menus open as bottom sheets below 640 px and next to their button (kept inside the screen) above. Dialogs (rename, share, sign-in confirmation, user administration, voice consent, label and assignee pickers, keyboard help) are full screen on phones with a pinned close button, and all overlays close with Escape or a tap outside and return focus to their button. Touch targets are at least 44 px, row menus are visible without hover on touch screens, long words and URLs wrap, the sidebar scrolls on short screens, and layouts respect the safe areas and the on-screen keyboard. A new CI job opens every screen and overlay at 360, 390 and 768 px in Chromium and WebKit (`make layout-check`). ([mobile-layout](openspec/changes/archive/2026-10-11-mobile-layout/proposal.md)) - **Contrast and accessibility** across the app: secondary text (sidebar section names, counts, dates, descriptions), the "All" links, primary buttons (now a darker green, white text at 5.5:1), status and priority colours and dark-mode muted text all meet WCAG AA (4.5:1) in light and dark. Secondary text uses two theme-aware tokens, `text-muted` and `text-faint`. Label and assignee pickers no longer nest buttons inside list options, the priority buttons say which one is selected (`aria-pressed`), and the filter and view result lists are named regions. The layout check now runs axe-core on every screen and overlay in light and dark, not only on the Help view, and fails on serious or critical issues. - Task and project **History** names people, projects, sections and parent tasks instead of showing their ids, labels fields in words ("assignee" rather than `assignee_id`), and no longer lists writes that kept the value, such as the plain-text copy of a note rewritten when the note is upgraded. - Zero-downtime updates work again: several server processes may now use one data directory, so Coolify's rolling update (new container started before the old one stops) no longer fails with "data directory is in use by another opentodo process" and rolls back. Background jobs (reminder push, webhooks, backups, email polling, Trash cleanup, audit pruning, demo reset) run in one process only and move to the new one when the old one stops; maintenance commands still require every server to be stopped. Upgrading from 0.1.0 needs one stop-then-deploy. ([Updating](docs/self-hosting/docker.md#updating)) ## [0.1.0] - 2026-10-11 The first release, and the first feature-complete version: every item on the [roadmap](ROADMAP.md) is implemented. Each feature was planned and built as an OpenSpec change; the archived changes under `openspec/changes/archive/` hold the full proposals, designs and task lists, and `openspec/specs/` the resulting behaviour contracts. ### Upgrade notes - **No mandatory configuration.** Every new environment variable is optional and every feature that talks to the outside world is off until configured. The complete list is in the [README](README.md#configuration). - **Database migrations** `0001` to `0024` are embedded and applied automatically at startup, in order, tracked by `PRAGMA user_version` (numbers `0016`–`0018` and `0020` are unused). Back up `/data` before upgrading an existing instance: there are no down-migrations. Rolling back to a binary older than user administration lets deactivated accounts sign in again. - **Update the server before clients.** The op schema only grew (new fields and entities, nothing renamed), but an older server rejects sync batches that carry fields it does not know; those ops wait in the device's outbox until the server is upgraded. Older clients keep syncing their personal data and skip ops for entities they do not know. - **Sizes** (measured for this release, `linux/amd64`): server binary 16,715,938 bytes (15.9 MiB) built with `-trimpath -ldflags="-s -w"`, web app embedded; `ot` CLI about 6.9 MB; initial web bundle 261 KB JS + 10.7 KB CSS gzipped, with the rich-text editor (97 KB), voice recognition (4 KB) and Vim layer (4 KB) loaded on demand; idle memory about 18 MB on a fresh instance. The container image adds the distroless static base (about 2 MB) to the binary. Release assets: server and `ot` binaries for Linux, macOS and Windows (amd64 and arm64), the browser extension zip and `SHA256SUMS`; images `ghcr.io/pixari/opentodo:0.1.0` and `:0.1` for `linux/amd64` and `linux/arm64`. ### Added **Core platform** - Go + SQLite server with the React PWA embedded, local-first storage in IndexedDB, field-level last-writer-wins op log, owner setup, sessions, API tokens, REST API that writes through the op log, JSON export, a single distroless container and a Coolify-ready compose file. [Architecture](docs/architecture.md), [sync protocol](docs/sync.md) - Live sync: a server-sent event stream (`GET /api/v1/sync/events`) wakes other devices as soon as ops are recorded; 30 s polling remains the fallback. [Live updates](docs/sync.md#live-updates), [proxy notes](docs/self-hosting/docker.md#live-updates-through-the-proxy) - Vim keybindings, opt-in per device and loaded only when enabled. **Capture** - Natural-language quick add in English, German, French, Italian, Spanish and Portuguese: dates, times, `#project`, `@label`, `p1`–`p4`, `+30m` estimates, with preview chips; parsed on the device. [Quick add](docs/quick-add.md) - Voice capture into quick add (`Shift+V`), opt-in per device, using the browser's speech recognition. [Voice](docs/quick-add.md#voice) - Share target, bookmarklet and a Manifest V3 browser extension for Chromium and Firefox (`make extension`), opening a capture screen at `/share`. [Capture](docs/capture.md) - Email to task: a personal secret address per account, polled from one operator-configured IMAP mailbox (`OPENTODO_EMAIL_*`). [Email](docs/email.md) - `ot` command-line client (`cmd/ot`), cross-compiled for Linux, macOS and Windows and attached to releases. [CLI](docs/cli.md) **Organisation and views** - Nested projects and subtask trees to any depth, with deterministic, write-free cycle breaking for concurrent moves. - Sections and a Kanban board per project; new `section` entity and task field `section_id`. [Sections](docs/api.md#sections) - Manual ordering by drag and drop and `Alt+↑/↓`, one `position` op per move. [Manual order](docs/sync.md#manual-order) - Labels as a synced entity referenced by id from `task.labels`. [Task labels](docs/sync.md#task-labels) - Filter query language evaluated on the device, and saved views and dashboards (`view` entity, REST `/api/v1/views`, validated by a Go port of the parser). [Filters](docs/filters.md) - Rich-text notes with collaborative editing: new task field `notes_doc` (a Yjs document envelope, up to 256 KB) next to `description`, which stays the note's Markdown projection for the API, export, CalDAV, MCP and webhooks. [Mergeable documents](docs/sync.md#mergeable-documents) - Trash with restore, revision history from the op log, permanent purge (`purges` table, `purged` in sync responses), automatic purge after `OPENTODO_TRASH_RETENTION_DAYS` (default 30) and automatic archiving of finished projects. [Trash and history](docs/api.md#trash-and-history), [Purge](docs/sync.md#purge) **Time** - Duration estimates (task field `estimate_minutes`) and Day/Week time-blocking calendars with drag, resize and full keyboard control. - Recurring tasks: task fields `recurrence` and `series_id`, one grammar implemented in Go and TypeScript against 180 shared vectors, convergent completion of the same occurrence on two offline devices. [Recurrence](docs/recurrence.md) - Reminders (`reminder` entity, relative or absolute) with in-app notifications, snooze and dismiss, and web push implemented with the standard library (VAPID, RFC 8291); key pair in `/push_vapid.pem`, `OPENTODO_PUSH=off` to disable. [Reminders](docs/reminders.md) - Time-zone-fixed due times: optional `tz` key in `due` (IANA zone, only with `time`), resolved by `internal/zoned` and the web app against shared vectors. [Zoned due values](docs/sync.md#zoned-due-values) **Collaboration** - Project sharing with viewer, editor, admin and owner roles: the op log is partitioned into sharing scopes, ops are authorised against the author's membership at receipt and refused ones are reported per op. [Sharing scopes](docs/sync.md#sharing-scopes), [API](docs/api.md#sharing) - Task assignment (`assignee_id`), the Assigned to me view and in-app notifications (`GET /api/v1/notifications`). [Assignment](docs/api.md#assignment-and-notifications) - Threaded Markdown comments (`comment` entity) with `@mentions`, and an activity log derived from the op log, per task and per project. [Comments](docs/api.md#comments-and-activity) - Activity feed across shared projects with unread counts (`project_prefs` entity, personal scope only; `GET /api/v1/activity`). [Activity feed](docs/api.md#activity-feed) - Invitations: single-use, expiring links in Settings → Members. [Invitations](docs/api.md#invitations) - User administration: admin role, deactivation, deletion, password reset links, owner transfer, own password change and `opentodo reset-owner-password` on the host. [User administration](docs/api.md#user-administration) **Integrations and data** - Data import of the app's own export, CSV and JSON exports of other task managers, with preview, duplicate detection and undo (`OPENTODO_IMPORT_MAX_MB`, default 20). [Import](docs/import.md) - Scheduled backups to a folder, S3-compatible storage or WebDAV with retention, optional `OTBK1` encryption, `OPENTODO_BACKUP_*` presets and `opentodo restore`; sync responses carry a log `generation` so devices re-send newer edits after a restore. [Backups](docs/backups.md) - Signed outbound webhooks with a durable queue, retries, delivery log and SSRF protection (`OPENTODO_WEBHOOKS_ALLOW_PRIVATE` preset). [Webhooks](docs/webhooks.md) - CalDAV two-way sync under `/dav/` with per-device app passwords (`otap_…`), off until the owner enables it (`OPENTODO_CALDAV` preset); new task field `ical_extra` keeps unmapped iCalendar properties. [CalDAV](docs/caldav.md) - Read-only MCP endpoint at `/mcp` for AI assistants (five tools, `opentodo://` resources), off until the owner enables it (`OPENTODO_MCP` preset). [MCP](docs/mcp.md) **Security and access** - Passkeys (WebAuthn) when `OPENTODO_BASE_URL` is https; verification in `internal/webauthn`. - Two-factor authentication (TOTP) with recovery codes, remembered browsers, an owner policy (`none`, `owner`, `all`), step-up re-authentication for sensitive routes and `opentodo mfa-reset` on the host. - Single sign-on with one OpenID Connect provider (PKCE, JWKS verification with the standard library), optional `OPENTODO_OIDC_*` overrides, account linking and an SSO-only mode with owner break-glass. [SSO](docs/self-hosting/sso.md) - Audit log of security events with retention, NDJSON export and opt-in client addresses. [What is logged](SECURITY.md#what-is-logged), [API](docs/api.md#audit-log) - Optional end-to-end encryption of task content per account (Web Crypto, AES-256-GCM, passphrase plus recovery key); routes under `/api/v1/encryption/`. [E2EE](docs/e2ee.md) **Launch** - Demo mode for a public demo instance: `OPENTODO_DEMO=true` creates a hidden owner and a shared demo account shown on the sign-in screen ("Try the demo"), seeds realistic data through the op log, wipes and reseeds it every night at 03:00 UTC (devices recover through purge markers, sessions stay), shows a banner, and refuses sign-up, account changes, integrations, import and encryption with 403 `demo_restricted`; no outbound requests. [Demo instance](docs/self-hosting/demo.md) ### Changed - Deleting a project only tombstones the project; its tasks keep their `project_id` and come back when the project is restored from the Trash. - API tokens have a scope: `read` tokens are refused on every write route and on `/api/v1/sync` (`403 insufficient_scope`); existing tokens stay `full`. - `/api/v1/sync` responses gained additive fields: `rejected`, `memberships`, `purged`, `trashRetentionDays` and `generation`; requests may carry `tzOffset`. Served ops carry `actor`. Older clients ignore them. - REST collections and the export include projects shared with you (with read-only `role` and `owner_id`), and the export gained `labels`, `views`, `reminders`, `sections`, `comments` and the `encryption` key record. The export format version stays 1. - `ot` embeds Go's time zone database so zoned due times resolve on hosts without one (Windows, distroless). - Server dependencies: `github.com/fxamacker/cbor/v2` (passkeys). Web dependencies: `qrcode-generator` (two-factor QR codes), `yjs` and the `prosemirror-*` packages (rich-text notes, lazy-loaded). Everything else uses the standard library and existing packages. ### Fixed - Imports no longer drop sections, recurrence and reminders. An own export now round-trips its sections, reminders and saved views with their ids (so section placement, recurring series and reminder links stay intact) and every task field, with recurrence stored canonical; reminders whose time has passed undelivered are imported dismissed so no stale notifications go out. Comments are reported in the preview but not imported (a comment keeps its author), and assignees other than you are dropped with a warning. CSV and the JSON formats of popular commercial task apps map recurrence strings (rule grammar, RRULEs, common spellings, structured objects; anything else is a preview warning), reminders (absolute and minutes before the due time) and sections. The `sections` import option gained `section`, now the default (`label` keeps the old behaviour); the preview counts new sections, recurring tasks and reminders; undo also removes imported sections, reminders and views. [Import](docs/import.md) - Reminders on tasks in a project in the Trash (or under a sub-project whose ancestor is in the Trash) no longer fire, neither as web push nor in the app. Restoring the project brings them back: a reminder whose time passed while the project was trashed is delivered once, late, like after server downtime. [Reminders](docs/reminders.md#trash) - `/api/v1/sync` pull pages, including the history replay for a newly joined shared project, are capped at about 4 MB of op values (always at least one op), so a batch of large rich-text notes no longer produces one huge response; `hasMore` asks the device to continue. [Sync](docs/sync.md#paging) - Purging a task, a project (with its tasks) or emptying the Trash by retention now removes the task's CalDAV resource name in the same transaction; migration `0024` removes rows earlier purges left behind. [CalDAV](docs/caldav.md) - A large import sent one webhook delivery per task. Imports and their undo now produce one `import.completed` or `import.undone` delivery per subscribed webhook with counts and project ids; ops written by an import no longer emit `task.*`/`project.*` events, so a webhook that wants imports subscribes to the new events. Ordinary writes keep their per-event deliveries. [Webhooks](docs/webhooks.md#imports) - Opening the app at a different host than `OPENTODO_BASE_URL` made passkeys fail with the browser's "origin is not authorized for the provided RP ID"; the app now explains which two hosts differ and what to change, and Settings → Security shows the host passkeys are tied to (also `passkeyDomain` in `GET /api/v1/auth/status`). In `docker-compose.yml` an `OPENTODO_BASE_URL` you set yourself now wins over Coolify's generated `SERVICE_URL_OPENTODO`. - Dropdown menus (view, section, project and label menus) could open mostly off screen on phones when the header wrapped; they now shift back inside the screen when opened. - The keyboard-shortcut help could not be scrolled or closed on phones (the panel grew past the screen on iOS Safari). It is now a full-screen sheet on small screens with a pinned close button, the overlay scrolls instead of the panel, and every other dialog overlay scrolls when its content is taller than the screen. - Clients skip ops for entities they do not know instead of failing the whole batch, so an installed PWA that has not refreshed yet keeps syncing when the server gains a new entity. - A reference to a deleted, foreign or not-yet-received section or label is displayed as "No section" or hidden without writing a repair op, so arrival order can never overwrite a newer edit. ### Security - Sign-up is closed after the owner account; new people join by single-use invitation links or, if allowed, single sign-on. - Sensitive routes (new API tokens, app passwords, invitations, user administration, two-factor and SSO settings, encryption key changes) require a sign-in or re-authentication within ten minutes on cookie sessions (`403 reauth_required`). - Rate limits on sign-in, registration, passkeys, the second sign-in step, re-authentication, invitation and reset previews, single sign-on, comment creation, admin actions and failed `/mcp` and CalDAV authentications. - Webhook delivery refuses loopback, private, link-local, metadata and other internal targets unless the owner allows private networks, and never follows redirects. - CalDAV accepts only app passwords; `/api/v1` refuses them. `/mcp` accepts only bearer tokens and checks `Origin`. - Credentials, secrets and audit details are kept out of logs and the audit log by an allow-list; secrets for backups and single sign-on live in `/secrets/` (mode 0600), outside the database and backups. - See [SECURITY.md](SECURITY.md) for the full model. ### Known limitations and verification follow-ups These features are implemented and covered by automated tests (unit, integration and in-process protocol tests), but could not be verified against the real thing in this environment: - **CalDAV** with real clients: Apple Reminders, Thunderbird and DAVx⁵ (Android tasks apps). - **Single sign-on** against a real OpenID Connect provider such as Authentik or Keycloak (tested against an in-process provider). - **Email to task** against real mail providers' IMAP servers (tested against an in-process IMAP server). - **MCP** with real AI assistant clients. - **Voice capture** with a real microphone in Chrome and Safari. - **Web push** on real devices and push services. - **Passkeys** on real devices and authenticators. - **Share target** behaviour in Firefox (and other platforms listed in [docs/capture.md](docs/capture.md)). - **Touch drag and drop** (board, manual ordering, calendar) on real touch devices. Other known limitations: CalDAV does not translate alarms into reminders (alarms survive round trips in `ical_extra`), MCP has no stdio bridge, comments are Markdown text rather than rich text, the browser extension is not published in the stores, and quick add does not parse recurrence or reminders. See the [roadmap](ROADMAP.md#future-ideas). ### Migrations | File | Adds | | --- | --- | | `0001_init.sql` | `users`, `sessions`, `api_tokens`, `ops` (the op log), `fields` | | `0002_invitations.sql` | `invitations` | | `0003_webhooks.sql` | `webhooks`, `webhook_deliveries`, the shared `settings` table | | `0004_passkeys.sql` | `passkeys` | | `0005_data_import.sql` | `import_jobs`, `import_items` | | `0006_audit_log.sql` | `audit_events` | | `0007_backups.sql` | `backup_runs`, the log generation | | `0008_project_sharing.sql` | `op_scopes`, `scope_fields` (replacing `fields`), `project_scopes`, `project_members`, `member_backfills` | | `0009_mfa.sql` | `mfa_totp`, `mfa_recovery_codes`, `trusted_devices`, `session_auth` | | `0010_reminders.sql` | `push_subscriptions`, a scheduler index | | `0011_task_assignment.sql` | `notifications` | | `0012_user_management.sql` | `user_status`, `password_resets`, the single-owner index and guard triggers | | `0013_trash.sql` | `purges` | | `0014_oidc.sql` | `identities`, `session_oidc` | | `0015_comments.sql` | a partial index for comments | | `0019_caldav.sql` | `app_passwords`, `caldav_resources` | | `0021_mcp.sql` | `api_token_scopes` | | `0022_email.sql` | `email_inboxes`, `email_receipts` | | `0023_e2ee.sql` | `encryption_keys` (the wrapped key record) | | `0024_caldav_orphans.sql` | nothing new: removes `caldav_resources` rows of tasks that were already purged | [Unreleased]: https://github.com/pixari/opentodo/compare/v0.8.0...HEAD [0.8.0]: https://github.com/pixari/opentodo/compare/v0.7.1...v0.8.0 [0.7.2]: https://github.com/pixari/opentodo/compare/v0.7.1...01b1fe0 [0.7.1]: https://github.com/pixari/opentodo/compare/v0.7.0...v0.7.1 [0.7.0]: https://github.com/pixari/opentodo/compare/v0.6.1...v0.7.0 [0.6.1]: https://github.com/pixari/opentodo/compare/v0.6.0...v0.6.1 [0.6.0]: https://github.com/pixari/opentodo/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/pixari/opentodo/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/pixari/opentodo/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/pixari/opentodo/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/pixari/opentodo/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/pixari/opentodo/releases/tag/v0.1.0