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
0035to0038(attachments of projects shared earlier move to the project; theadmintoken scope; confirmed pages of a joined project's history; an index for dependencies) are applied automatically at startup. Back up/databefore upgrading: there are no down-migrations. - No new environment variable is required, but set
OPENTODO_BASE_URLif you have not. Password resets, sign-up confirmations and invitations are emailed only when it is set, and with it set the app must be opened at its host (the sign-in screen names the expected address otherwise). With an https base URL the server also sends HSTS. (Outgoing email) - Reverse proxies and client addresses. Proxies on loopback, private or container networks, and
OPENTODO_TRUSTED_PROXIES=cloudflare, need nothing. If you setOPENTODO_TRUSTED_PROXIESto your own addresses, the list now replaces the default: add the new keywordprivateif a reverse proxy on the machine or a container network also connects (for exampleprivate, 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. BehindOPENTODO_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_TOKENis set: it is printed in the log (docker logs opentodo). From the same machine or your local network nothing changes, and servers that already have an owner are not affected. (The setup code) - API clients and scripts. A
fulltoken is refused on the credential and administration routes with403 insufficient_scope: use anadmintoken there (see Breaking). Request bodies are checked strictly, so a field a route does not define is refused with400 bad_request. Sign-in answers429 account_lockedafter repeated wrong passwords, and password checks can answer503 busy. (API) - Sync stays compatible: upgrade the server before the devices, as for every release. Changes the server makes itself now come from the device
server:<id>(one id per server process; webhooks still report the sourceserver). 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: 30sin your compose file (ordocker stop -t 30) to let a backup or a digest finish cleanly. - Image tags. The release publishes
ghcr.io/pixari/opentodo:0.7.0and:0.7forlinux/amd64andlinux/arm64, and moves:stable, which most templates anddocker-compose.ymlnow follow. Installs on:0.6stay on 0.6: change the tag tostable(or:0.7) to update. The templates for stores that need a version (CapRover, CasaOS, Easypanel, Helm, Runtipi, TrueNAS, Umbrel) name0.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
fulltoken on credential or administration routes must switch to anadmintoken (breaking for those scripts only; everything else, including reading these settings and lists, revoking tokens and app passwords, and forgetting remembered browsers, works withfulltokens as before). Such requests now get403 insufficient_scope. Create an admin token in Settings → Integrations → API tokens → New token, access Admin (after confirming your password if asked), orPOST /api/v1/tokenswith{"scope":"admin"}from a signed-in session, and put it in the script instead.ot token createneeds an admin token in the profile too;ot loginstill works. Existing tokens keep their scope. (API, CLI)
Security
- A server can no longer turn end-to-end encryption off on your devices. If the server says it has no encryption key record (for example after it was restored from an older backup), your devices keep encrypting and pause syncing instead of uploading your content in plaintext. A screen lets you restore the key record from the device, turn encryption off on that device, or sign out. When you turn encryption off on one device, each of your other devices now asks once before following. (End-to-end encryption)
- Encryption key records with unusual settings are refused. A device only unlocks with a key record whose key derivation strength is within the expected range; otherwise it stays locked and says why.
- Only the share sheet can hand files to the installed app. Other websites can no longer store shares in the app through its share target, and a share larger than 100 MB in total is not stored. (Capture)
- The encryption threat model now notes that the server can show an older version of an encrypted field. Content stays unreadable and unforgeable to the server, but the app cannot tell when it is served a previous encrypted value of the same field. (End-to-end encryption)
- Pasting into notes is safer. The rich-text editor's ProseMirror view is updated to 1.42.6, which fixes a published issue with pasted content (GHSA-c8x8-7fp4-3x9w).
- Links are encrypted with the rest of your data. Turning end-to-end encryption on now also encrypts the address and name of links you attached before, and turning it off decrypts them again. Each link is replaced by a copy in the new form; the old one moves to the Trash. (End-to-end encryption)
- A damaged or tampered backup archive cannot fill the disk during a restore. Every file in the archive must have exactly the size its manifest lists, nothing more is written, and the server checks that the listed files fit before it extracts anything. (Backups)
- Images and release files can be verified. The container image is signed with cosign (keyless, tied to the release workflow) and carries an SBOM and build provenance; every file on a GitHub release has a build provenance attestation for
gh attestation verify. (Running with Docker) - A release is published only when every check passed. The image, its
stabletag 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.modtoolchain, 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; thegocommand 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'sCF-Connecting-IP) are believed only when the connection comes from a trusted proxy: by default loopback, private and link-local addresses, which covers a reverse proxy on the same machine or a container network. A client that reaches the server directly can no longer choose the address that rate limits, the audit log and the demo limit see. IPv6 clients are limited per/64. (Behind a reverse proxy) - Repeated wrong passwords lock the account for a while. Ten failed passwords, two-factor codes or re-authentications for one account within 15 minutes, from any addresses, lock its password sign-in for the rest of those 15 minutes (the app says so; the API answers 429
account_lockedwithRetry-After). A successful sign-in clears the count. Passkeys, single sign-on and existing sessions keep working. - A new server cannot be claimed from the internet without a code. Without
OPENTODO_SETUP_TOKEN, creating the owner account from a public address now needs a one-time setup code that the server prints in its log when it starts without an owner (docker logs opentodo). From the same machine or your local network nothing changes. The code is forgotten once the owner exists. (The setup code) - Stricter origin check with a base URL. With
OPENTODO_BASE_URLset, 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
Securewhenever the request arrived over https, also without a base URL. With an httpsOPENTODO_BASE_URL, responses sendStrict-Transport-Securityfor one year (noincludeSubDomains, 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
busywithRetry-After. Sign-in refuses passwords over 1,024 bytes without checking them. (API) - Sign-in timing no longer tells accounts without a password apart (single sign-on and demo accounts): they cost the same check as an unknown address.
- Live-update streams are capped: 10 per account (a new one closes the oldest), 20 per client address and 2,000 per server; beyond the last two the stream is refused with 503
busyand the app keeps syncing by polling. (Sync) - One account's push endpoints cannot hold back everyone's reminders. Reminders are sent for 8 accounts at a time with a 20 s budget per account and pass, an account keeps at most 20 push subscriptions (a new one replaces the least recently registered), and a subscription that failed 20 times in a row and delivered nothing for a day is no longer sent to until the device re-enables notifications. (API)
- Calendar data, MCP batches and calendar subscriptions are bounded. Reading iCalendar data (CalDAV writes and subscribed calendars) takes time in proportion to its size, and one content line may be at most 64 KB. The MCP endpoint refuses JSON-RPC batches under protocol revisions that no longer have them (2025-06-18 and later), accepts at most 20 messages per batch otherwise, and stops running a batch once its answer reaches 4 MB. Expanding a subscribed calendar's repeating events starts at the shown period, shares one work budget and a 5-second deadline per calendar, and keeps what it found with a warning when it runs out. (CalDAV, MCP, Other calendars)
- Emailed links always use the server's public address. Password resets, sign-up confirmations and invitations sent by email are built only from
OPENTODO_BASE_URL, never from the request, so nobody can make the server mail a link to another site. WithoutOPENTODO_BASE_URLthese emails are not sent: the screen says the owner must set it, links to copy keep working, and the owner sees a configuration warning while outgoing email is set up. (Outgoing email) - Forwarded host headers count only from trusted proxies.
X-Forwarded-HostandX-Forwarded-Protoare used only when the request comes from an address inOPENTODO_TRUSTED_PROXIES. Behind a TLS proxy, setOPENTODO_BASE_URL(or list the proxy) so the links the app shows start withhttps://. (Docker) - A shared project's id cannot be taken over. Sharing a project is refused when another account has a project with the same id, so nobody can lock the owner out of their own project. (Sync)
- Entity ids are limited to safe characters. Letters, digits and
. _ : @ + = ~ -, which covers every id the apps create, so a malformed id can no longer break the lists of a shared project's members. (Sync) - Credentials and administration need an admin token or your signed-in session. A
fullAPI 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 newadminscope, which can only be created from such a session. A leakedfulltoken can no longer turn off your second factor, mint more credentials or issue reset links. (API tokens) - Single sign-on never links the owner, admins or accounts with two-factor authentication or a passkey by email alone. Those accounts sign in as usual and link the provider in Settings → Account → Linked accounts; until then the provider sign-in is refused with "This account is not linked yet". Members without a second factor are still linked by their verified email. (Single sign-on)
- A password reset forgets remembered browsers, so every browser asks for the two-factor code again, whether the link came from Forgot password? or from an administrator.
- Sign out everywhere. Settings → Account → Session → Sign out everywhere (
POST /api/v1/me/sign-out-everywhere) ends every session of your account, this one included, forgets remembered browsers and revokes your API tokens and CalDAV app passwords at once. It is recorded in the audit log assessions.revoked. (Account security) - Every route declares who may call it. Each API route is registered with one policy (anonymous with a reason, signed in, owner, owner and admins, accepted token scopes, re-confirmed session, refused on a demo), replacing the separate owner and session checks that disagreed (outgoing email refused tokens while single sign-on accepted them; both now follow the rule above). A test fails for any route without a policy and checks the demo restrictions against it.
Added
- Templates for six more ways to run OpenTodo. A Helm chart for Kubernetes (one pod with a persistent volume, an optional Ingress, a generated setup token, a locked-down pod), published with each release to
oci://ghcr.io/pixari/charts/opentodo; a Podman Quadlet that runs OpenTodo as a systemd service, kept up to date bypodman auto-update; a CapRover one-click app; afly.tomlfor Fly.io; a Render Blueprint with a Deploy to Render button; and a Container Manager project for Synology DSM 7.2+. Each is checked bymake checkand has a guide. (Kubernetes, Podman, CapRover, Fly.io, Render, Synology) - Templates for eight more platforms. CasaOS and ZimaOS, Cosmos Cloud, Dokploy, Easypanel, Portainer, Runtipi, TrueNAS Community Edition and Umbrel each get a template in their own format, checked by
make check, with a guide covering install, settings, data permissions, https, updates and backups. Until a platform's catalog lists OpenTodo, the guide shows how to install it as a custom app; Portainer and Dokploy can already load the templates from this repository by URL. Stores that refuse moving tags name the release version (Umbrel also its digest, refreshed byscripts/deploy/pin.mjs); the others usestable. (CasaOS, Cosmos, Dokploy, Easypanel, Portainer, Runtipi, TrueNAS, Umbrel)
Changed
- The task list, calendar, task properties and assistant panel are built from smaller parts. Nothing changes in how they look or work; their grouping rules now have their own tests, and a check keeps new screens from growing into one very long function again.
- For contributors: the deploy template checks share one strict YAML parser (
scripts/deploy/yaml.mjs) instead of four that read some files differently, so a quoted'it''s'or a folded text means the same in every check. The committed templates are unchanged. - For contributors:
AGENTS.mdanddocs/architecture.mdname every server package,AGENTS.mddescribes the MCP server's write tools, and a repository check fails when a package or token scope is left out. The privacy list in the README now also names outgoing email and calendar subscriptions. - OpenTodo is in Unraid's Community Applications. Install it from the Apps tab: search for OpenTodo and click Install. Installing from the template URL by hand still works. (Unraid)
- A
stableimage tag, used by every template. Each release now also publishesghcr.io/pixari/opentodo:stable(release candidates never move it), and the Unraid and Coolify templates,docker-compose.ymland 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.6keep their tag and its fixes; switch tostableonce to follow every release. A Promote stable workflow pointsstableat an earlier release to roll back. (Docker, Unraid, Coolify) - Choose the address
docker-compose.ymlpublishes on with the optionalOPENTODO_BINDin.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.1is the safer choice. (Docker) - Contributors: the server code is easier to find your way in. The HTTP server, the sync write path, quick add, the importer, CalDAV and the AI agent tools are split into smaller files and functions, with no change in behaviour. A test now fails when a function under
internal/grows past 150 lines or a file past 1,100 lines. - Separate rate limits per purpose. Sign-in, registration, password resets, invitation links, public links, MCP token failures and passkey sign-in each have their own window of 10 a minute per address, and successful sign-ins no longer count, so guessing one kind of link no longer blocks signing in.
- Request bodies are checked strictly. A field a route does not define is refused with 400
bad_requestnaming 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 413too_large, and error messages no longer show the server's internal decoder text. The app and theotCLI send only defined fields. (API) - The last-use time of API tokens and app passwords is updated at most once a minute, which saves a database write on every request.
- The outgoing-email settings (
GET/PUT /api/v1/settings/smtp) now accept anadmintoken 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
#nameof an archived project no longer behaves differently in the sheet and in a list; every screen now reads projects, labels and shared-project roles from one shared live copy, so they agree with each other and update together after a sync. - Screen readers hear what the chips say. Counts, "Has notes", deadlines, group counts and the warning on a title that could not be decrypted were silent or read as bare numbers; they are now read as full phrases ("3 comments", "4 open tasks").
- Screen readers follow the list. Moving with j / k moves focus to the selected task, subtasks are read with their level and position, and each complete button names its task ("Complete Buy milk"). (Keyboard shortcuts)
- Page changes are announced. The browser tab names the page ("Today – OpenTodo"), going to another page moves focus to its heading, and a "Skip to content" link jumps past the sidebar. (Keyboard shortcuts)
- The log says which level it uses. The startup line now ends with
log_level=, and withOPENTODO_LOG_LEVEL=debuga first debug line says what debug adds (one line per API request). Before, a log read right after a restart looked the same atdebugas atinfo, so the setting seemed to be ignored. (#128) - Project pickers no longer list projects twice. With only a few projects, a picker (such as "Tasks without a project go to" when importing) showed the ones picked before under "Recent" and then again in the full list. Short lists now show each project once; longer lists keep "Recent" on top and head the full list "All". (#136)
- Buttons and menu items are named by the text they show (WCAG 2.5.3 Label in Name), so voice control finds them by what is on screen: menu items no longer repeat their shortcut or note in the name, group headers are named by their title and count (collapsed or expanded is announced as a state), calendar items, month days and the project's member list read their full description without hiding the visible text, and "Make owner" and "Restore this value" start with their visible words.
- Unraid: Tailscale explained. The container's Use Tailscale switch cannot work with the OpenTodo image (it has no shell for Unraid's Tailscale script, so the container stopped with
exec /opt/unraid/tailscale: no such file or directory). The Unraid template and guide now say to leave it off and give the steps for Tailscale Serve on the Unraid server, which gives OpenTodo an https address on your tailnet. (Unraid, #123) - A server opened at an address other than its base URL says so. After a move to a new domain with
OPENTODO_BASE_URLstill naming the old one, signing in and "Try the demo" were refused with "cross-site request rejected" or "This page expired. Reload it", which a reload does not fix. The sign-in screen now names the address the server is set up for, and the log has one warning per refused host naming it and the base URL. (Behind a reverse proxy, #125) - A proxy page no longer loses unsynced changes. When something between the app and the server (a proxy, a captive portal) answered a sync with its own page, changes made offline were dropped. They now stay on the device and upload on the next sync, and the status says the answer was not understood.
- Changes are kept when a sync turns out to need unlocking. If encryption was turned on on another device during a sync, changes a server limit held back were lost and refused changes kept their local values. They are now held for later or take the server's values.
- Custom fields survive a server restore. After the server is restored from a backup, devices now send their custom field definitions back with everything else.
- Refused custom field and link changes are handled. A custom field change the server refused now shows the server's value, and a refused attachment no longer stops syncing.
- Stopping the server no longer interrupts its background work halfway. On shutdown the server now waits (up to 20 seconds) for its background jobs before closing the database: a backup in progress is cancelled and recorded as failed instead of failing with "database is closed", and a weekly digest or reminder that went out just before shutdown is recorded, so it is not sent a second time after the restart.
- Backups no longer fill the disk. A backup checks the free space first and fails at once with "not enough free disk space for a backup" and the space it needs. Snapshots and partial archives left by a crash are removed when the server starts, and backups to a folder on the server are written straight into that folder instead of being copied there from the data directory. (Backups)
- Email to task leaves other mail clients' deleted messages alone. On a mail server without the UIDPLUS extension, OpenTodo no longer expunges the mailbox (which removed every message marked for deletion, not just its own); processed messages stay marked read and deleted until the mail server or your client removes them. (Email to task)
- Reminders on completed tasks no longer slow the server down over time. The server records them as dismissed once instead of checking them again every 30 seconds, and reads due reminders in pages. (Reminders)
- Large backups reach S3 and slow destinations. Archives over 100 MiB are uploaded to S3 in parts, so archives over S3's 5 GB single-upload limit work, and an upload is no longer cut off after 30 minutes: it fails only when the destination stops answering for 5 minutes. Give the S3 key
s3:AbortMultipartUploadtoo. (Backups) - Old read notifications are removed while the server runs. Read notifications older than 90 days are now removed daily, not only when the server starts.
- Signing out during a sync no longer leaves the previous account's data behind. A sync still waiting for the server is stopped before it writes anything more, and the next account starts with a fresh device. Other open tabs of the app reload to the sign-in screen. (Offline and sync)
- A server that stops answering no longer stalls syncing. Every request now gives up after 30 seconds (10 minutes for file uploads and downloads); the status shows offline and the app tries again shortly, waiting a little longer each time. (Offline and sync)
- Open tabs recover after a server update. A screen whose code changed in the update reloads on its own, a "Reload to update" prompt appears when a new version is ready or another tab updated the stored data, and an error in one screen or panel no longer blanks the whole app. The server now answers missing app files with "not found" instead of the app page, and only a real network problem shows as offline. (Troubleshooting)
- Changes that can't be made say why. Moving a task under one of its own subtasks, a move a section does not allow, or a change that fails to save now shows the reason at the bottom of the screen instead of doing nothing.
- Only one tab syncs. With the app open in several tabs, one of them syncs for all; the others show its status and take over when it closes. Moving between lists no longer starts a new sync each time. (Sync protocol)
- Changes held back by a server limit are no longer lost or sent twice when they are offered again at the moment another change is held.
- A title you are typing stays as you type it when another device renames the task at the same time; leaving the field saves your title.
- The browser is asked to keep the app's data, so it is not deleted when space runs low or after weeks without a visit.
- A device with a wrong clock no longer freezes a field. When a device's clock runs more than five minutes fast, the server now asks it to correct its clock; the app stamps the change again and uploads it, so the edit is kept and later edits from other devices win as they should. The app also stops correcting its clock once the system clock is right again. Older apps keep the previous behaviour. (sync)
- Two server writes in the same millisecond no longer cancel each other. Every change the server makes itself (API, assistant, reminders, imports, email) now uses one clock, and the server's device id carries a suffix per server process (
server:<id>; webhooks still report the sourceserver). - 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 checkand 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 checknow 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
jandknow 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
icommand now puts the caret in the task's title, and moving through the list withjandkreleases 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,
sandenow 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://andmailto: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.mdnow says, as the server does, that the audit log takes the client address from the lastX-Forwarded-Forhop (the one your proxy appended), withOPENTODO_TRUSTED_PROXIESfor 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.
Install
- Docker:
docker run -d -p 8080:8080 -v opentodo:/data ghcr.io/pixari/opentodo:0.7.0 - Binary: download
opentodo-<os>-<arch>below, then run it (data goes to./data). - Command-line client:
ot-<os>-<arch>. Browser extension:opentodo-browser-extension.zip.
Verify downloads with sha256sum -c SHA256SUMS. Self-hosting guides: docs/self-hosting.