Static demo — actions are disabled. Source and install
TasmoFleet
What’s new
TasmoFleetv0.36.0
Account · maren What’s new
Theme

What’s new in TasmoFleet

You’re on v0.36.0

0.36.0

“Fisgard” InstalledNew since your last visit
Upgrade notes
  • Migration 0048 adds the columns start_at and not_started to ota_jobs, so an update job can wait for a start time; every existing job keeps starting at once. Nothing to do (#603).
Added
  • You can plan an update for later. The update preview has a "5 · When" card: start now (the default) or at a chosen time, with the quick picks Tonight 03:00, Tomorrow night 03:00 and "Like last time", or any other date and time. The time is read in your browser's time zone (in the server's without JavaScript), a time that is not in the future is refused and the button reads "Schedule update"; the audit log records the planned start. The help of the "Send when" notification setting now says that "problems" also sends a scheduled update that was not started (#604)
  • Update jobs can wait for a start time. At the start TasmoFleet checks every device again (the safety guard, the device lock, a firmware update still running, OTA exclusion). If TasmoFleet was down at the start time, the update starts as soon as it is back, but only within 60 minutes; later it is not started at all and a "Scheduled update not started" notification is sent (also with "only problems"). A planned update can be cancelled until it starts (#603)
  • Planned updates show up in Transactions. A "Scheduled" section above the history lists them soonest first with their start, the time left and who planned them, and a Cancel that says nothing on the devices changes. The page of a plan shows its start, what happens if TasmoFleet is down then and its devices as "Planned"; an update that started says how close to its time it started. A plan that TasmoFleet was down for shows "Not started" with the reason and the notification text, a cancelled plan "Cancelled before start", and both offer "Plan again…" (the update preview with the same devices and release) instead of Retry. Until it starts a plan is never shown as queued or running (not in the status bar, the Dashboard or About), the device stays usable, and the device picker, Make compliant and deleting the device name the plan that holds it; a delete is refused before "Delete the backups too" runs. A device that was busy for the whole late-start window, or excluded from updates after planning, now reads "Device was busy" or "Excluded from updates since the plan" instead of "Guard refused" (#604, #326)
Changed
  • The built-in SQLite is now 3.54.1 (was 3.53.4); System, About shows the version.
Fixed
  • Every control a person starts now shows that it runs, and a failed request says what failed: a file chosen for the import, a field and the live rule preview show a ring while they wait (they are never disabled, so the newest value is still sent), a request that got no answer shows a toast naming the control ("Refresh identity was not sent: no connection", "… timed out"), the "Still working" note closes as soon as its request ends, and the links and buttons into the restore, drift and replace pages, which read a device before they answer, show a ring until the page arrives (#634)
  • The Devices list after a live reload: the table is fitted in the same step that the reload swaps it in (the Power column used to lie on the row menu in every row until the page had settled), and resizing a column with the arrow keys while the list refreshed no longer resets the whole table to equal-width columns, because the width is saved from the table that is on the page (#727)
  • The Firmware page no longer shows tooltips of several hundred characters that list every device name, over the device count of a file and over a cached file that many devices use: they name as many devices as fit in 160 characters and say how many more there are (#727)
  • Pages no longer scroll about 120 px past their content: the leftover bottom padding of the page container is gone, so a long page such as Devices ends one normal gap above the status bar, and the device list's selection bar rests one gap above the status bar at the end of the scroll as it does while scrolling (#773)
Removed
  • The old container package ghcr.io/nofuturekid/tasmofleet is deleted. It stopped at 0.34.1; pull ghcr.io/hellwand/tasmofleet instead. The Docker Hub image nofuturekid/tasmofleet is not affected (#738)

0.35.0

“Bishop Rock”
Upgrade notes
  • Migration 0047 adds the table id_high_water, seeded with the highest device and transaction id that the database or the audit log knows, so the ids of devices and transactions deleted before the upgrade are not taken again either. Nothing to do. Ids that were already reused before the upgrade cannot be repaired: such a device or transaction keeps showing the older audit rows of its predecessor (#498).
  • The old container package ghcr.io/nofuturekid/tasmofleet will be deleted with 0.36.0. It stopped at 0.34.1; pull ghcr.io/hellwand/tasmofleet instead. The Docker Hub image nofuturekid/tasmofleet is not affected (#738)
Changed
  • The rule editor's hint for Latitude and Longitude now says that the value is compared to 4 decimals (so 48.2082 and 48.208200 are the same), gives the range, and that a whole number such as 48 means 48.0000, not a prefix (#382, thanks @Noschvie)
  • The tooltip table holds only the words a badge can show; unreachable entries, including the retired Warn wording, are gone (#516)
  • GPIO templates: the reference device of a group is always its first row, with its own Reference chip and a tinted row, whether it was chosen or is the most common layout; the other devices keep their id order (#547)
  • System, Snapshots now says that the times in the snapshot file names are UTC (the trailing Z), with an example, and the configuration and backup docs explain the same; the names themselves are unchanged (#683, thanks @Noschvie)
  • The message about a failed scheduled backup now lists each failed device with its hostname, address and the reason it failed, so you can tell from the message whether you need to act, and its link opens the failed devices on the Backups page (#685, thanks @Noschvie)
  • A failed Settings read of a device now writes one warning to the log with the device id, the query that failed and the reason as a short class (timeout, unreachable, auth and so on); the page text is unchanged and the line never holds a setting value or a credential (#712)
  • Start warning when a sign-in variable (AUTH_MODE, PUBLIC_URL, OIDC_*, ADMIN_GROUPS, OPERATOR_GROUPS, OIDC_CLIENT_SECRET_FILE) is set in the environment without the TASMOFLEET_ prefix: such a variable was ignored without a word; the warning names the key that works, the value is still ignored and never logged (#721)
Fixed
  • A deleted device or a transaction removed by the retention (AUDIT_RETENTION) no longer frees its id for the next new one: a new device took the id of the newest deleted device and showed its console history and audit rows, and old links and notifications opened it; a new transaction could show another one's audit rows the same way. Ids only grow now (#498)
  • The style guide's Severity sample is built from the app's own severity control, so it no longer shows the stronger level (warn) as Advisory (#514)
  • The style guide page has one h1, one Breadcrumb landmark and unique ids: the samples of the page header, crumbs, error page and sign-in card draw their title and trail without the page-level identity (#515)
  • The backup age metric of a device no longer counts the safety backup of replacement hardware that "Replace device" files under it: it shows the device's own last backup, and a device with only such runs has no backup age sample (#545)
  • The one-time backup layout migration now treats a file system without directory fsync (vboxsf, some 9p and SMB mounts) like the backups themselves do: it updates the row in the same run instead of leaving the entry for the next start, and logs the limitation once per directory; real I/O errors still leave the entry (#548)
  • GPIO templates: the group cards no longer jump around when the devices are read again; the groups stay in the order of chip family and template name, and each card keeps its column, whatever a device answered, on a desktop, a tablet and a phone. The summary line and the row chips still show what differs (#549)
  • Health: a device on your broker that sends no telemetry no longer flips to Stale and back between its polls when POLL_INTERVAL is longer than about 9 minutes (at 15 minutes that wrote about 190 history rows a day, at 2 hours the device showed Stale most of the day), and a start no longer writes two history rows for each such device about 11 minutes later. A poll's answer, or its refused HTTP API, now counts until the next poll is due; a device the poll no longer reaches is Stale from that poll on (#669)
  • The Devices list on a phone shows every filter chip and picker without sideways scrolling: the chips wrap onto further lines, the filter field fills its whole line below 720 px, and the separator in front of the Chip and Tag pickers is hidden below 1120 px, where the row can wrap (#678)
  • The 24 h Wi-Fi sparkline of a device's Stability card fills the whole width of its column at every screen width instead of stopping at 240 px (#679)
  • The info icons of the Identity card's "Reported by the device" fields sit on their label's line instead of below it, and a tap on one opens its explanation (a tap elsewhere or on another icon closes it); on a phone the text could not be read before (#680)
  • CI: superseded Go caches no longer pile up on main and push the repository over the 10 GB cache limit. A job after main.yml, the nightly and the weekly run keeps the newest cache of each series (Go version and go.sum hash do not count) and deletes the rest (#692).
  • Policy reads of OtaUrl, PowerRetain, LogHost and SwitchMode1 to SwitchMode28 go over MQTT first like every other read, so a device that is online on the broker but whose HTTP does not answer can be checked against these rules; who may send them in the console is unchanged (#711, reported in #702, thanks @Noschvie)
  • Add devices: "Show ignored" now counts and lists a dismissed address change or conflict too, in its own section (Address changed or Conflicts) with an Ignored badge and Restore as its only action, and Restore puts it back there. Before, such a dismissal could not be undone: the count skipped it, and the device keeps announcing the same data, so the 30-day expiry never removed it (#714)
  • Discovery: a device whose tasmota/discovery/<MAC>/config message cannot be used (not JSON, no valid MAC or IP address, a MAC that differs from the topic) is now named in the log at the default level, once, with its MAC and the reason, never the message text; before, the debug line named neither. A broker reconnect that replays the same message does not write it again. MQTT messages that find the discovery queue full (discovery configs, LWT, STATE, INFO, Status 5 replies) are counted in tasmofleet_discovery_proposals_dropped_total{kind="queue_full"} (#715)
  • Audit log: the entries for ignoring and restoring a discovered device now say which device it was. The target shows its name and the number, the summary its kind and address, and the details its MAC; before, they only read "Discovered device #12". Older entries keep their text (#716)
  • A button that updates only part of a page (such as Test connection or a relay switch), pressed after the session expired (a tab left open past the idle limit), now leads to the sign-in page instead of putting the text "missing or invalid CSRF token" into the page in place of the row or card (#717)
  • Add devices: Ignore, Ignore selected and Restore now carry the version of the device the page showed and are refused with "The form is out of date. Reload the page and try again." when the number has since been given to another device. Before, a page left open could hide (or restore) a device that appeared after it was loaded and that nobody had seen, because the numbers of deleted rows are reused (#745)
Security
  • The hostname a device reports is now cleaned like the rest of its text before it is stored (control and invisible characters removed, at most 100 characters). Before, a device, or anything answering at its address, could store a hostname with line breaks, invisible or direction-changing characters, or of any length, which then showed on its pages. The hostname listed in the message about a failed scheduled backup is cleaned as well, also for hostnames stored by older versions (#747)
  • The Unicode line and paragraph separators (U+2028, U+2029) are now removed from device names and hostnames like the other control and invisible characters. Before, a name or hostname containing one was kept and could show as a second line in the message about a failed scheduled backup; discovered devices were affected in the same way (#755)

0.34.3

“Split Rock”
Fixed
  • The Devices list keeps up with its live updates on larger fleets: a cell is only swapped when its content changed, and the select-all sync no longer runs for every single cell (#726)
  • The :edge image on ghcr no longer disappears after a re-run of the same commit: replacing the previous edge keeps every manifest that a tagged image still uses (#728)
  • CI: the weekly mutation run opens its failure issue when a shard hits the job timeout too, and it deals its files over five shards by fresh weights, so no shard runs into the limit (#729)

0.34.2

“Split Rock”
Added
  • docs/pitfalls.md: one rule per mistake that already happened, with where it is enforced (#525)
Changed
  • CLAUDE.md is a short router with the enforcing command per rule; the details moved to docs/dev, and make agent-refs fails when they name something that does (#526)
  • Changelog lines are added as fragment files in changes/unreleased/ instead of edits to CHANGELOG.md, so parallel pull requests no longer conflict there; the release commit assembles them with make changelog-assemble (#527)
  • A second ruleset on main, "main: CI must be green (no bypass)", holds the status check required without an admin bypass, so nothing reaches main before required is green on that commit (#709)
Fixed
  • Settings: the tab could not be read on a device that is online on TasmoFleet's broker but does not answer over HTTP ("The device did not answer in time."); the MQTT user was asked over HTTP first, and that timeout discarded the whole read. A bare MqttUser is now read like every other value, MQTT first; in the console it stays admin-only and its reply masked (#702, thanks @Noschvie)
  • Settings, Sign-in: "Test discovery" showed the whole settings page inside the Identity provider card; it now answers with its own result line only, like the other test buttons (#705, thanks @Noschvie)
  • The Authentik guide printed the sign-in variables without the TASMOFLEET_ prefix for the container configuration, where a name without the prefix is ignored without a message (the short names are valid only in /data/tasmofleet.env); it now uses the prefixed names, says that issuer and client secret must be set in the same place and that a setting comes from the page or from the container configuration, names the "Sign-in" tab, and marks the application icon as an optional Authentik-side step that tasmofleet neither sends nor reads (#706, thanks @Noschvie)
  • Settings, Sign-in: the help under Issuer URL no longer says the issuer needs a trailing slash (that is Authentik's form); it must be exactly the issuer the identity provider publishes (#718)
  • Settings: every hint that names a container variable now shows the TASMOFLEET_ name, which is the only form the container environment reads (a short name there is ignored without a message): the help of locked fields, "Needs TASMOFLEET_PUBLIC_URL", the save error, the confirmation banner and the "Set by the container" card (#719, thanks @Noschvie)
  • Sign-in: with only the client secret in the environment, the start log now has one warning that TASMOFLEET_OIDC_ISSUER is missing because the secret locked it, and the Sign-in tab says so instead of "set by the environment"; the same for an issuer from the environment, which locks the client secret. docs/configuration.md states that both are set as variables or neither (#720, thanks @Noschvie)
Security
  • Docker Hub credentials are read only through the dockerhub deployment environment (policy: main and v* tags); pull requests and other branches never receive them, and missing credentials on main or a release tag now fail the job instead of silently skipping Docker Hub (#693)

0.34.1

“Split Rock”
Upgrade notes
  • The repository moved to github.com/hellwand/tasmofleet. Docker Hub users (Unraid included) change nothing. ghcr.io users: switch the image to ghcr.io/hellwand/tasmofleet; this release is also published once more under the old name ghcr.io/nofuturekid/tasmofleet so you see this note, later releases are not. No database migration.
Added
  • Website: a project page in site/ (static HTML and CSS, light and dark, with the real UI screenshots, the features, install per platform and links), for GitHub Pages at the root with the demo next to it (#566)
Changed
  • The GitHub Pages site now serves the website at https://hellwand.github.io/tasmofleet/ and the static demo moved to https://hellwand.github.io/tasmofleet/demo/. Links to the old demo address land on the website, whose demo link leads on (Fixes #566).
  • The README is a landing page (pitch, features, install in a few lines, links to the website and demo). Installation details, backing up, what TasmoFleet changes on devices and development moved to docs/ (#565)
  • The repository moved to the hellwand organization. The ghcr image is now ghcr.io/hellwand/tasmofleet; the old ghcr.io/nofuturekid/tasmofleet keeps its existing tags but gets no new ones, so change the image name if you pull from ghcr. The Docker Hub image nofuturekid/tasmofleet is unchanged, so Unraid users change nothing (#690)

0.34.0

“Split Rock”
Upgrade notes
  • Migration 0046 (adds the table snapshot_runs; data otherwise): a stored snapshot interval that is not zero becomes every day at 03:00 (SNAPSHOT_DAYS all seven days, SNAPSHOT_TIME 03:00) and the interval row is deleted; a stored 0 (off) and no stored interval stay off. Nothing to do: look at System, Snapshots, Schedule, and choose other days or another time if 03:00 does not suit you. The migration also stores a marker so that the first start after the upgrade does not take a snapshot (#602, #601).
  • TASMOFLEET_SNAPSHOT_INTERVAL no longer exists. It is not in the Unraid Community Applications template, so a template install has nothing to change. Where you still set it (Compose, docker run, an env file) it is ignored and TasmoFleet logs a warning at start; delete it (#602).
Changed
  • Scheduled database snapshots are taken on chosen weekdays at a time of day, set on System, Snapshots under Schedule (weekday chips and a time, on the server's clock, TZ), instead of every so many hours. No day ticked is off. The time stays the same on the wall clock when the clocks change (#602).
  • TasmoFleet does not take a scheduled snapshot when it starts, and saving the schedule does not take one either. Until now every start and every change of the interval took one at once, even minutes after the last (#602).
  • A scheduled snapshot that TasmoFleet missed because it was not running is taken late when it is back within 60 minutes of the scheduled time. After that it is skipped and a notification is sent (the existing "Database snapshot failed" setting, linking to System, Snapshots), and the Schedule card says so until a newer snapshot exists (#602).
Fixed
  • Reachability: the leftovers of a device whose HTTP API is off (SetOption128 0) and which MQTT cannot serve. Upgrade <url> and WebGetConfig <url> typed in the console or sent in bulk are refused up front for such a device, by the router's own rule that jobs never go over MQTT, instead of failing with the refusal. Refresh identity is disabled when the refusal is found while the page is open, and enabled again when it ends, without a reload. The GPIO templates refresh, the policy check and the group-topic reader leave such a device alone like an offline one, without an error. One wording everywhere: restore, Replace, the update retry, the import report, the backup history, job notes and logs say "HTTP API off (SetOption128 0)" with the fix. A "no path" for an update no longer advises an upload over MQTT, which no page offers. Cosmetic: Restart's disabled state no longer points at the "Back up:" note, its refusal page is titled "Restart not sent", a refused relay switch shows the reason once, and the text no longer breaks inside "(SetOption128 0)" on a phone (#659)
  • A console command, switch, settings save, Make compliant, bulk command or backup, update, restore or Replace device that meets TasmoFleet's own background read of the device waits up to five seconds for it instead of being refused with "Another operation on this device is running (broker read)" (since 0.29.1; since 0.32.0 the read also probes the identity and holds a device about two seconds: after the device restarts, for every device of a fresh installation or after a power cut, and one device every 30 seconds after TasmoFleet starts). Any other busy device still refuses at once (#670).
  • UI: the values of the stat tiles on the device overview and in the Policy header are drawn in the tone's text colour instead of its fill, so amber and grey values meet WCAG AA (4.5:1) on the light surface and the dark one; the design system now states the contrast target (#513)
  • Import from TasmoAdmin: the plan counts lines and shows a problem that keeps its row (an unusable port, an encrypted password without the key file) as a note on that row. Before, a devices.csv with 1000 such rows showed "0 reject" next to a red row "Reject · 1950 more lines with errors", because it counted problems and called warnings rejections. A blank line after exactly 1000 rows is no longer refused as "more than 1000 rows". tasmofleet import prints the notes with their line and says how many rejected lines it does not list one by one (#661)
  • Last known values: a console command that changes a setting and Make compliant now count as writes, so a read that started before them is no longer shown as the current values on the Settings tab. The background read of a device's values stops when TasmoFleet stops (it kept sending to the device for up to a minute), and values another device answered (a MAC other than the stored one, at the stored address or on the device's MQTT topic) are no longer remembered for the Settings tab or the policy check; the read fails instead The Settings tab of a device on minimal firmware (an update in progress), which cannot show its MAC, says so instead of "could not be read" (#662)
Breaking
  • The setting and variable SNAPSHOT_INTERVAL is removed without an alias; use SNAPSHOT_DAYS and SNAPSHOT_TIME (System, Snapshots). See the upgrade notes above for what happens to an existing interval (#602).
  • A settings file written by tasmofleet settings export before this version holds SNAPSHOT_INTERVAL, and tasmofleet settings import refuses the whole file for it (setting is not editable here). Delete that line before you import the file, then choose the days on System, Snapshots (#602).

0.33.0

“Dondra Head”
Upgrade notes
  • Migration 0045 (data only): a stored backup interval that is not zero becomes every day at 03:00 (BACKUP_DAYS all seven days, BACKUP_TIME 03:00) and the interval row is deleted; a stored 0 (manual only) and no stored interval stay manual only. Nothing to do: look at Devices, Backups, Schedule, and choose other days or another time if 03:00 does not suit you. The migration also stores a marker so that the first start after the upgrade does not run a backup (#601, #612).
  • TASMOFLEET_BACKUP_INTERVAL no longer exists. It is not in the Unraid Community Applications template, so a template install has nothing to change. Where you still set it (Compose, docker run, an env file) it is ignored and TasmoFleet logs a warning at start; delete it. It had only been read once, on the first start that saw it, and the migration has already carried the stored value over (#601).
Added
  • Notifications: browser notifications while a TasmoFleet tab is open. The events of the webhook and e-mail channels (a finished update, a failed scheduled backup or snapshot, as far as Settings, Notifications selects them) also show to admins as desktop notifications. Each browser opts in itself, with "Enable browser notifications" on that tab (the permission is asked for only after the click; the choice stays in the browser, nothing is stored on the server), and with several tabs open one notification comes per event. The card says so when the browser blocks notifications or the page is not HTTPS or localhost, where browsers do not offer them. No service worker: nothing shows while no tab is open (#540)
Changed
  • Spec: the operator role reads the redacted values in config diffs (backup comparison and Drift tab); only free-text settings (rules, scripts, Berry code, mems, rf_code) show just that they changed. The code and the restore security notes already worked this way; a handler test now pins it (#501)
  • Scheduled device backups run on chosen weekdays at a time of day, set on Devices, Backups under Schedule (weekday chips and a time, on the server's clock, TZ), instead of every so many hours. No day ticked is manual only. The time stays the same on the wall clock when the clocks change (#601).
  • TasmoFleet does not run a fleet-wide backup when it starts, and saving the schedule does not run one either. Until now every start (a container restart, an update, a Home Assistant app restart) and every change of the interval backed up all devices at once, even minutes after the last run (#612, #601).
  • A scheduled run that TasmoFleet missed because it was not running starts late when it is back within 60 minutes of the scheduled time. After that it is skipped and a notification is sent (the existing "Scheduled device backup failed" setting), and Devices, Backups says so under Last run (#601).
Fixed
  • Policy: "Show all" under the devices of a rule draft works on the first click right after typing, instead of the form's own preview putting the short list back (#660)
  • Health: devices whose retained last will says Offline are no longer all polled on the first tick after a start (the polls are spread over one poll interval), a will that arrives live (a reboot, an OTA) is polled one interval later instead of while the device restarts, and the first poll after a replayed will no longer writes an "Online" history row per device. A device on MQTT whose telemetry is quiet is polled every POLL_INTERVAL like a device without MQTT, so it stays Online instead of going Stale and back every 11 minutes (HTTP API on) or 30 minutes (HTTP API off) while POLL_INTERVAL stays below the stale threshold (as with the defaults), and the back-off cap no longer undercuts a POLL_INTERVAL longer than 30 minutes (#652)
Breaking
  • The setting and variable BACKUP_INTERVAL is removed without an alias; use BACKUP_DAYS and BACKUP_TIME (Devices, Backups). See the upgrade notes above for what happens to an existing interval (#601).
  • A settings file written by tasmofleet settings export before this version holds BACKUP_INTERVAL, and tasmofleet settings import refuses the whole file for it (setting is not editable here). Delete that line before you import the file, then choose the days on Devices, Backups (#601).

0.32.0

“Fastnet”
Added
  • Device data stays current on its own: once a day per online device (spread over the fleet, one device at a time) and after a restart (once, 20 seconds after it settled; a device in a boot loop about once an hour), TasmoFleet reads the Settings values, the values the policy rules check and the GPIO template reading, so the Settings tab and Policy → GPIO templates show them without a click, and it probes the identity again over HTTP after a restart and every 24 hours (without renaming anything: names and topics change only on Refresh identity). Offline and busy devices are skipped, the policy values are not read while the live check is paused, and Status 0 is never sent (#600)
Fixed
  • Import from TasmoAdmin: a row with a problem that does not reject it (an encrypted password without the key file, a bad port) counts once against the 1000-row limit instead of once per problem, so a devices.csv with 501 to 1000 encrypted rows is no longer refused with a false "more than 1000 rows" (#500)
  • A device whose HTTP API is off (SetOption128 0) and which MQTT cannot serve is now refused up front, with the same reason and fix, by everything that used to try and fail: its relay switches and Restart (disabled on the device page with the reason), Refresh identity, direct posts to Console and Settings, retrying a backup, command or Make compliant transaction, Make compliant on cached values, applying a GPIO template, the Drift tab's live read, and the silent policy read every ten minutes (skipped without an error). One wording everywhere ("HTTP API off (SetOption128 0)" and the fix, also in Copy from a device) replaces "likely refuses HTTP API commands", and the fix no longer reads as if it meant MQTT. The connection card's "Since" shows when the API was first found off, not the latest refusal. Bulk Send command applies the write rule only to commands that write (#645)
  • Stopping TasmoFleet (docker stop, a restart) with a browser tab open takes a moment instead of 10 s and exits with status 0 instead of 1, so restart policies and monitoring no longer read it as a crash; and a start that fails halfway now waits for the background tasks before it closes the database (#497)
  • Policy: "Show all" under the devices of an unsaved rule (from a template, a command or Duplicate) works without JavaScript and keeps what you typed, instead of ending on a white page reading "unknown form field"; a mistyped address of New rule or Duplicate shows the error page too (#512)

0.31.0

“Cape Spear”
Changed
  • The Settings tab and Policy → GPIO templates show the last values read at once, with their age ("Read 3m ago"), instead of waiting for the device, and read it in the background when they open; values older than an hour carry a "Stale" marker, a refresh that fails keeps the old values and says why, and opening a page while a read runs sends the device nothing more. The values are kept in memory, so after a restart the first open reads the device again (#599)
Fixed
  • Sign out after the session has expired signs out: the sign-out button no longer ends on a white "403 missing or invalid CSRF token" page, the cookie is cleared and you land on the sign-in page (#508)
  • A device on TasmoFleet's broker that left it with a retained Offline will (a power cut while switching broker, a restored config for another broker, MQTT switched off) is now asked over HTTP like any other device, at the normal interval while it answers and backing off while it does not, so it is shown online again when it answers and the broker hints and re-reads can reach it; before, it stayed Offline forever. With its HTTP API off as well, the refusal counts as an answer, as for every other device: it is online, the Reachability tile says "HTTP API off", and backups, updates and bulk actions say up front that no path is left (#619, #447, thanks @Noschvie)
  • What's new: a long section title such as "Upgrade notes: action may be required" wraps inside its badge instead of running over the first item, paths in code break after a slash instead of inside a word on phones, and the collapsed "Older releases" list no longer reserves 8 px below its title (#510)
  • Firmware: a host-style device name such as livingroom-readinglamp.example.internal in the Delete file confirmation breaks between its labels on a phone instead of inside one (#511)
  • Layout at tablet widths (720 to 1199 px): the Devices table of Backups uses the full width instead of being cut off in a half column, Settings → MQTT puts TLS on its own row (and Notifications puts the SMTP security on its own row) so "Reset to default" no longer overlaps Port, the Notifications message preview no longer breaks the word "failed", and a policy rule has no double gap above its history; on Backups, device names and "Newest backup" now wrap at every width where they reach the end of the line instead of being clipped (#633)

0.30.1

“Eddystone”
Fixed
  • Times no longer show in the server's format for a moment while a page loads, for example when switching the Online and All chips on Devices (#635, thanks @Noschvie)
  • Devices with SetOption4 1 are read over MQTT without the 8 s wait and the HTTP fallback every 10 minutes, and writes to them over MQTT are confirmed (#632)

0.30.0

“Eddystone”
Changed
  • Reads go over MQTT first for devices on TasmoFleet's broker that are online there: the console's and bulk commands' read-only commands, the Settings tab, policy reads and the read-back after Make compliant, the GPIO template reads, the Status 1 read of the GroupTopic, the network facts (hostname, IP, Wi-Fi) and the identity check of the update preview are asked over MQTT, so devices HTTP cannot reach (the HTTP API switched off, another network) answer them too, and a device's reads no longer wait for an HTTP check. A device that is not on this broker, is not online on it or whose topic is shared is read over HTTP as before. HTTP is tried once after MQTT when nothing was published (the channel refused the command, or the device was offline) or when the device did not answer a published read (devices with SetOption4 1 answer where TasmoFleet does not listen); such a device is then read over HTTP for ten minutes while HTTP works. Changes (settings, Make compliant, console writes), restores and updates keep their order: HTTP first, MQTT when HTTP fails; a backup reads its status snapshot over the transport that carried its dump (#456)
  • HTTP reachability is checked on demand instead of aging unseen: a device that is fine on MQTT is not polled over HTTP, so opening its page now asks for one HTTP check (at most one per device every 5 minutes, never while a job or lock holds the device), and so does the periodic broker re-read for a device whose HTTP fact is not fresh. The Reachability tile, the connection card and the device list word the result the same way ("not checked yet" / "not checked lately", "HTTP API off"), and the HTTP row no longer promises "tries HTTP first" (#456)
  • Every action shows at once that it is running: the button you clicked is disabled and shows a spinner (plain forms until the next page arrives, htmx buttons until the answer is there; a switch only dims, and nothing spins when the system asks for reduced motion), so a second click does nothing; after about 10 s a note says it is still working, also for a section that loads from the device by itself (#538)
Fixed
  • About and Settings → Advanced: on a tablet (768 to 1199 px) the two configuration tables no longer sit side by side; the setting names overlapped the values, so they now stack in one column (#592)
  • A device whose HTTP API is switched off (SetOption128 0) is no longer counted as offline: a refused call is an answer, so the device stays online in its header and in the device list, the Reachability tile says "HTTP API off" with "The web page answers" (with live MQTT: "Connected · HTTP API off"), and the connection card and the list's HTTP channel name the refused API. With MQTT off as well, its last will keeps it offline, and the offline banner now says that its web page answers and what to switch on. Such a device is polled about every 30 minutes (the page's check gives news on demand). When MQTT cannot serve it either, it has no path: scheduled backups skip it with the reason "HTTP API off (SetOption128 0)" (no failure, no notification), and updates, bulk actions and a manual Back up refuse it up front with that reason and the fix (SetOption128 1, or add the device by its own IP or hostname); a device that MQTT can serve is not refused, and is backed up and changed over MQTT for as long as its HTTP API is off. A check that reaches a device its last will reports offline no longer moves "Offline since" (#447, thanks @Noschvie)
  • "Reset to default" in the settings shows the default at once: after the click the field holds the value Save will store and says "Default, set on save", instead of the old value greyed out; a reset switch (a stored on/off setting) turns to its default immediately, and typing in the field afterwards cancels the reset. The schedule intervals (Devices, Backups and System, Snapshots) no longer offer a Reset: their default is off, which is what the schedule switch does, so resetting the interval could silently switch a schedule off while the switch still said On. Without JavaScript a ticked reset now wins over the value posted next to it instead of refusing the save (#623)

0.29.1

“Portland Head”
Fixed
  • The broker a device reports to is kept current. TasmoFleet used to store it only when it probed the device, so a device moved to another broker outside TasmoFleet (its own web page or console, a restored config that points to an old broker) kept the old one and the "Other broker" hint never appeared until someone clicked Refresh identity. For each device TasmoFleet reaches over HTTP, the broker is now re-read after every restart TasmoFleet notices, after an MqttHost or MqttPort command sent through TasmoFleet (console, bulk command, settings, Make compliant), and about every six hours, one small Status 6 read at a time and never while a restore, update (one typed in the console included) or backup works on the device; a read that fails or says nothing about MQTT changes nothing, so the "Other broker" hint follows a move made outside TasmoFleet or by a restore without Refresh identity (#598)
  • Device page and device list: no "Other broker" hint for devices on TasmoFleet's broker under another name (the Home Assistant app's core-mosquitto, Docker service names): a broker counts as TasmoFleet's when devices that use it are heard there (#597)
  • Devices on a Tasmota build with a localized web UI (German, French, …) are identified and can be updated; TasmoFleet no longer expects the English footer (#468, thanks @t4nki)
  • ESP32 file restore on a Tasmota build with a localized web UI: the device's reply is read without depending on the language, so a restore no longer ends "file lost" when the file was written back (#618)

0.29.0

“Portland Head”
Upgrade notes
  • Migration 0043 adds the tables backup_runs and backup_run_items: the last scheduled backup run is stored, so the status bar, the dashboard and Devices, Backups show it after a restart. It starts empty (#409).
  • Migration 0044 adds the column since_at to backup_run_items (when a skipped device went offline). Earlier run records keep their text and show no time (#409).
Added
  • Devices, Backups: one page for how the fleet is backed up (#409). It shows the last scheduled run with the failed and the skipped devices apart, one row per device (newest backup, runs, files, result of the last run) with filters for No backup, Older than, Failed last run and Skipped, the storage totals and what the next run prunes, the schedule, and the backups of deleted devices. Admins can back up the selected devices, or every device that a scheduled run would try ("Back up all now"): the next page lists the devices it leaves out with the reason, and it also leaves out devices that are offline right now or whose web password was refused; operators see the page without the run buttons and the schedule form (they can open backups and devices, and keep or release the backups of deleted devices). Devices has a new sub-navigation entry between Policy and Add.
Changed
  • Device pages follow the card-width rule: Activity is one full-width card with its entries in two columns (three on a large screen), and Replace device puts the form in the wide stack and "What comes along" and "What happens" beside it; on the preview "Copied to the new device" sits under the form and "Not copied" and the start step beside it. The four always-copied options are now only shown on the first Replace page, and the preview no longer lists the files option (the Files row and the confirmation text still name the files); nothing about what Replace does or confirms changed (#419)
  • New update, Firmware and release pages: the cards now follow the Dashboard's content width in column stacks instead of leaving half the page empty. The Devices and Options cards each have two inner columns, the Plan no longer sticks while you scroll, cached firmware files take two lines with a short checksum, and the custom firmware drop zone is a card of its own (#419).
  • System pages fill the content width. Users shows the list in the wide column with the roles (and how many active people hold each) and the safeguards as cards beside it, and the last sign-in as a day ("Yesterday 21:10") instead of an age. About puts the version and the licence links next to the components. What's new lists the releases in the wide column, with "You are on" and a jump list of every release beside them, staying in view while you scroll. The Settings pages use the whole content width instead of stopping at 1240 px, and General and Discovery use two columns instead of leaving the third empty (#419)
  • Scheduled backups skip devices that cannot answer instead of counting them as failed. A run leaves out devices that never answered, devices that are offline (the reason says since when, with the date) and devices another operation or an update is working on; it tries the rest, including devices that refuse the web password (they fail: you have to act). After a restart a device that was offline before is still skipped. The status bar and the failure message count only the devices that were tried ("2 of 5 failed"); a run that skipped everything neither colours the bar nor sends a message. The last run is stored (migration 0043, kept as long as the audit log) and shows again after a restart. A device is locked against other operations while it is backed up (#409)
  • The status bar's backup item links to Devices, Backups and counts skipped devices apart ("22 of 24 backed up · 2 failed · 3 skipped"); on a phone the line shows the short form and the sheet the full text. A run that only skipped devices stays calm (#409).
  • Dashboard: the "No backup" tile opens Devices, Backups narrowed to the devices without a backup and says when the last scheduled run was. It turns amber only when that run failed for a device, never for skipped ones (#409).
Fixed
  • Settings, Notifications: the preview caption names the whole request, and the preview shows a link only when a message carries one (PUBLIC_URL set); the caption no longer promises a link in the e-mail without PUBLIC_URL (#579)
  • Device page: the "Other broker" hint now also shows for a device that used to report to TasmoFleet's broker and moved to another one (a broker migration, or a restored config that points to an old broker). Before, the stored "on our broker" state and the retained last will kept such a device "heard" for good, so the hint never appeared. A device now counts as being on our broker only if it sent something there within the last 2 hours; one that answers HTTP but has been silent longer shows the hint. A device that does not answer HTTP is left alone, since it may just be switched off. After TasmoFleet restarts or reconnects to its broker the retained last will counts as a message, so the hint can take up to 2 hours to appear. It also needs TasmoFleet's record of the device's broker to be current: after a restore or a change made outside TasmoFleet, use Refresh identity (#580, thanks @Noschvie)
  • Device page: the Reachability tile says "MQTT on another broker" for a device that reports to another broker, instead of "MQTT not configured", in line with the "Other broker" card (#580, thanks @Noschvie)
  • The notification of a failed scheduled database snapshot linked to the device backups (and, since 0.27.0, to a Settings tab that no longer held the snapshot schedule); it now links to System, Snapshots (#409).
  • Devices list and other live pages: times, the select-all controls and the transaction log's scroll position no longer flash on each refresh (#594, thanks @Noschvie)
Breaking
  • Settings, Backups is gone: its storage, schedule and deleted-device cards moved to Devices, Backups. The address /settings/system?tab=backups now opens the first Settings tab, and a link to the old tab (a bookmark) needs to point to /devices/backups (#409).

0.28.0

“Cape Byron”
Upgrade notes
  • TasmoFleet no longer switches relays back on after a firmware update (#551, thanks @Noschvie). If you relied on that, set SetOption0 1, PowerOnState 3 and SaveData 1 on those devices (a Policy rule can do it for the fleet and "Make compliant" applies it; the template "Restore power after an outage" covers PowerOnState 3). Devices that already keep their relay state are not affected.
  • Webhook notifications: if you send to ntfy (or Gotify), open Settings, Notifications and set the webhook Format to ntfy (or Gotify). Until then the webhook keeps sending JSON, which ntfy shows as raw text (#469, thanks @t4nki)
  • Migration 0042 (data only): update jobs that were waiting in the state restore_power end as succeeded with a job event. The power columns of ota_jobs stay in the database unused (#551, thanks @Noschvie)
Added
  • Notifications: the webhook Format now offers ntfy and Gotify next to Generic JSON and Home Assistant. ntfy gets a plain-text message with a title, a link to the TasmoFleet page (when PUBLIC_URL is set) and a priority that stands out for failed or partial results; Gotify gets title, message, priority and the link as the click action. The Message card previews what the saved format sends (#469, thanks @t4nki)
  • Dashboard: a "No backup" tile counts the devices that have no backup yet, the same number as the device list's "No backup" chip, and opens that list. It is blue, not amber: a device that never answered has no backup either, so the number is not a failure count. The dashboard now also refreshes when a backup is taken or deleted (#438, thanks @Noschvie)
Changed
  • The update dialog has no "Power after update" choice any more, the preview shows no relay line, and a job no longer has a "Restoring power" step. TasmoFleet does not read or switch relays around an update: no Power command, no Status 11 reading for relays, no PulseTime/Interlock/ShutterRelay reads, no extra MAC read after the reboot. The README section "What TasmoFleet changes on your devices" says so (#551, thanks @Noschvie)
  • Device page: the MQTT broker no longer appears as a row of the Connection card (Wi-Fi or Ethernet). The "How TasmoFleet reaches this device" card names the device's broker only when it is a different one than TasmoFleet's ("Other broker", with the address the device reports to), because such a device does not show up live. Nothing is shown when MQTT is switched off in TasmoFleet, when the device reports to TasmoFleet's broker under another name or address, or when the device has no broker (#441, thanks @Noschvie)
Fixed
  • Devices on the ESP32 safeboot image or the ESP8266 minimal firmware are no longer asked for settings they do not know. They answer Unknown to nearly every command, so the hourly policy reads and the live check, "Copy from a device", the changes-since-backup view, the Templates tab's reads, the group topic read and the network facts read each filled their log and MQTT topic with errors (SetOption128, TimeZone, Sleep, Status and the rest of the rule commands), and a backup run sent its 17 snapshot commands after the dump. None of these goes to such a device any more; the health poll and the device page's Settings tab are unchanged. TasmoFleet now notices from a device's INFO1 message that it booted into safeboot (the (safeboot) or (minimal) build tag, whatever the version, chip or prefix) or left it, shows the device in the policy as "Not judged" with the firmware named (an older reading or backup never judges it), and checks it again about once an hour with an identity refresh, so a device that is back on regular firmware is read again; a regular INFO1 ends the state at once. A backup run for such a device ends with the reason instead of failed commands (#558, thanks @Noschvie)
  • Applying a GPIO template now refuses a device that runs the safeboot image, like the minimal firmware, with a reason that names safeboot, instead of sending a Template command it answers with Unknown (#560)
  • Deleting a device while a job runs on it no longer makes the update engine log ERROR ota transition not persisted ... conflict (or not found). The delete is now refused while an update, command, backup, policy or template job or a restore is unfinished on the device: the delete page says what is running, links to the transaction (or the restore) and offers no Delete button, and a post answers 409 and deletes nothing, and the backups you chose to delete with the device stay too, because the check comes first. Only when a job starts in the moment between that check and the delete itself are the backups already gone; the 409 page then says so. The refusal is in the audit log. The database does the check and the delete in one step, so a job that starts at the same moment cannot slip through (the device stays). Before, the delete took the running job's row with it; the job's worker then failed to save its next step, and when SQLite had reused the job's id for a new job it reported a conflict instead. A job whose row is gone is now a single warning, not an error (#466)
  • Drift no longer reports sensor readings as differences. The Drift tab compared the whole Status 10 block (temperatures, power and energy counters, the sensor clock) between the stored snapshot and the live device, so a device with a sensor was always "drifted". Status 10 is now left out of the comparison and of "Show all", like the other live groups (#446, thanks @Noschvie)
Breaking
  • The setting OTA_POWER_MAX_AGE (Settings > Firmware & updates > Updates (OTA), TASMOFLEET_OTA_POWER_MAX_AGE) is gone. A stored value is deleted by migration 0042; the environment variable is ignored (#551, thanks @Noschvie)
  • The drop reason ota_hint_invalid_relay of tasmofleet_mqtt_dropped_total is gone, because POWER reports no longer go to the update engine (ota_hint_queue_full and ota_hint_unknown_topic stay). Alert rules or dashboards that match it must drop it (#551, thanks @Noschvie)

0.27.4

“Cordouan”
Fixed
  • Make compliant no longer reports settings as failed that the device applied, and no longer loses settings at the next power cut. Tasmota runs a Backlog from a queue, one line per SetOption34 delay (200 ms by default), and over HTTP it answers before the queue has run. The read-back right after the send therefore saw the old value of every line but the first ("SetOption0 is 0, expected 1; SaveData is 0, expected 1") on devices that had applied all of them, and "Retry failed" passed only because the values were already set. A value that differs is now read again, first after a quarter second per differing setting, then up to five more times a second apart (the last read counts, so a value the device really refuses still fails, after about six seconds plus a quarter second per differing setting). The save that follows the changes (sent when the change has no SaveData of its own and does not restart the device) was a plain command, which Tasmota runs at once, before the queued lines: they ran after the save, so they were not written to flash and were lost at the next power cut on a device with SaveData 0 (a Restart 1 saves first). It is now queued behind the lines (Backlog SaveData <value>). The simulator now runs a Backlog like Tasmota (#559)
Security
  • GPIO templates: the Template command's NAME and ARCH are now encoded as JSON strings instead of concatenated into the command (CodeQL go/unsafe-quoting). Accepted names produce the same command byte for byte; names with a quote or backslash are still refused (#561)

0.27.3

“Cordouan”
Upgrade notes
  • Metrics: tasmofleet_backup_last_success_timestamp_seconds has a new id label (the inventory id) next to device (#499). Queries and recording rules that join or aggregate this metric by its exact label set (on(device), ignoring, sum by (device) over devices that share a name) must add id; a plain {device="..."} selector keeps working. The bundled Grafana dashboard is updated.
Fixed
  • The audit log no longer misses a change when the browser disconnects right after you submit it. The audit row was written with the request's context, which is cancelled when the client goes away, so a device deleted or a setting changed just before a closed tab or a proxy timeout had no audit row (only an error in the log and tasmofleet_audit_errors_total). The row is now written independently of the request, with a time limit of its own (#505)
  • Make compliant no longer leaves the first part of a change unsaved when a later part fails. A long change is sent in several chunks and saved once at the end; when a later chunk failed, the earlier ones were live on the device but never written to flash, so on a device with SaveData 0 (or a long save interval) they vanished at the next restart, and the error did not say that part of the change was already sent. The part that went out is now written to flash first (with a time limit of its own, so a run that used up its deadline can still save), and the job's error says which settings were sent (not read back), which step failed and may or may not have arrived, and which were not sent, and says so when the save failed too. When TasmoFleet is stopping, the save is skipped, or ended at once if it is already running, instead of holding up the shutdown; the error says the settings were not written to flash because TasmoFleet was stopping, and the job sends and saves the whole change again at the next start (#503)
  • A relay you switch off while a device updates is no longer switched back on by "Power after update" when many MQTT messages arrive at once. Since 0.27.2 a POWER OFF report that found the queue of waiting reports full was dropped, and the restore then did not know you had switched the relay off. While TasmoFleet runs, a POWER OFF report of a device in the inventory is no longer dropped: waiting reports are kept one per device and relay, the device is matched from the inventory's topics when the report arrives (no database read in between), and the update engine records a report that finds its job busy instead of dropping it. Topics that are no device of the inventory, and relays outside 1 to 32, are dropped and counted in tasmofleet_mqtt_dropped_total as ota_hint_unknown_topic and ota_hint_invalid_relay, so junk on the broker cannot fill the waiting set or crowd out a device. Only an UPGRADE report may still be dropped when the queue is full (it only ends a wait early). The reports wait in memory: a restart of TasmoFleet in the middle of an update still loses the ones that had not been handled yet (#534)
  • The device list could show a relay in its old state after you switched it, when the list reloaded itself (for example after the first probes of a new device) at the same moment, and kept it until the next poll. A reload that was rendered before the switch finished is now dropped and requested again (#463)
  • Backups: the safety backup of a restore or a replace can no longer delete the backup you are restoring. With a full history the chosen run was the oldest, so the retention that follows the safety backup removed it, and a restore the device refused left nothing to retry with. The chosen run now stays through the retention of the backups the restore or replace takes itself. Also, the safety backup of the new hardware in "Replace device" (filed under the dead device) is counted apart from the device's own backups, so failed replace attempts no longer push its real configurations out until "no backup" is left (#494)
  • Backups: a backup run that fails while its files are written or indexed (disk full, database refusing a write) no longer leaves the files it had already stored. Before, the run was reported as failed and a "Failed" row appeared, but the device's list also showed a new run with only the dump: it looked like a good backup, could be restored, and counted against BACKUP_KEEP, pushing an older complete backup towards pruning. A failed run now stores nothing (#496)
  • Backups: a backup file is now flushed into its directory before it is indexed. Before, only the file itself was synced; after a power loss right after a backup the Backups tab could list a run whose file "is missing" when you opened or restored it, because the rename had not reached the disk but the index row had. A file whose directory entry cannot be made durable (a real error such as EIO) is no longer indexed, and the run fails. A file system that has no directory sync at all (some VM and network shares answer EINVAL, ENOTSUP or ENOSYS) is logged once per directory and the backup goes on as before (#506)
  • Metrics, label change: tasmofleet_backup_last_success_timestamp_seconds now carries an id label (the inventory id) next to device. Before, the series was keyed by device name only, so two devices with the same name (Tasmota's default is Tasmota) shared one series and the last one written won: an alert on backup age watched whichever device came last and missed the other's stale backup. Each device now has its own series, {device="name",id="7"}; without "Device names in metric labels" the single fleet series is {device="fleet",id=""}. Queries and Grafana panels that match on device alone keep working, but a query that compared or joined the series by the exact label set (on(device), ignoring, a recording rule) must add id; the bundled dashboard's legend now shows {{device}} {{id}} (#499)

0.27.2

“Cordouan”
Fixed
  • An update whose device answers again after the restart but whose firmware cannot be read (Detect or Status 4 / Status 11 fail every time) no longer runs for ever. The reboot window never ended it, so it held its update slot and blocked every further job for that device. It now fails when the window is over, with "device is back but its firmware cannot be read" and what failed, and sends no second Upgrade (#493).
  • An update step no longer fails because the firmware cache is busy. While a firmware index fetch was running (the index expiring, the firmware page opened, Refresh, a slow source: up to two minutes per source), Ensure waited for it even for a file that was already cached and ignored its own deadline, so the OtaUrl or Upgrade that followed ran on an expired 30 s context and failed with a timeout that had nothing to do with the device. A cached file is returned at once now, and waiting for a fetch or for another download of the same file ends with the caller's context (#495).
  • An update whose Upgrade 1 reply names another URL than the one TasmoFleet set (another tool changed OtaUrl in between) still fails, but no longer releases a device that is flashing. That reply means the device accepted the upgrade and downloads from the other URL; the failed job let go of it at once, so a new update could start on it and overlap the first flash. The device is held against new updates now until it has restarted or 10 minutes have passed, like after a typed firmware command (#347); the refusal says the flash was started by another tool (#504).
  • Recording that a device reports to our MQTT broker no longer overwrites an edit made at the same moment: an admin who saved a new address, password or name just as the device's first message arrived got the old values back, and the audit log showed a system update that hid the change. The capability is now written only if the record is still as it was read, and read again otherwise. The other way round, saving the edit form no longer puts back what the application recorded after the form was loaded and the form does not edit (the MQTT capability, the topic): the save is applied again to the fresh record when it changed (#502)
  • MQTT reports of a device's UPGRADE result and of a relay switched OFF are handled by one background worker with a bounded queue, in the order they arrive. Before, every such message started its own goroutine and database lookup that shutdown did not wait for, so a scene or a broker reconnect with many relays reporting OFF at once caused a burst of lookups, and a lookup could still run against the store and the update engine while the application was shutting down. A report that does not fit in the queue is dropped and counted in tasmofleet_mqtt_dropped_total{reason="ota_hint_queue_full"}; the update engine uses both reports only as hints (#507)
  • Language builds: the firmware language of a device is now looked up in a table of the upstream builds instead of being derived by upper-casing the lang of its main page. Before, a device on one of eight builds was matched to a file that does not exist (Catalan AD, Czech CZ, Greek GR, Swedish SE, Vietnamese VN, Chinese simplified CN and traditional TW looked for CA, CS, EL, SV, VI, ZH, ZH-HANT-TW) and could not be updated without choosing the language by hand, and a device on the Brazilian build was updated with the European Portuguese file. Both Portuguese builds report the same code pt on the main page; their locale ID in Status 4 tells them apart (0416 Brazil, 0816 Portugal), so both are recognised as long as Status 4 answers. If it does not, or a code is not in the table, the device is not guessed: it waits for you to choose its language in the update preview, like a device that only MQTT reaches. The language choice now offers Lithuanian, which was missing, and names the two Portuguese builds "Portuguese (Brazil)" and "Portuguese (Portugal)" (#509)

0.27.1

“Cordouan”
Upgrade notes
  • No database migration (the schema stays at 0041). Nothing to change in the template.
  • Four security fixes (GHSA-fvgh-83hf-5jr9, GHSA-cpfg-6c57-xg33, GHSA-fmj9-3vh6-rq53, GHSA-wq89-cqcx-3h3h), listed under Security below. Update when you can.
Added
  • ESP32-H2 is a known chip family (esp32h2). It has no Wi-Fi and upstream builds no image for it, but an own build that runs over Ethernet can now be uploaded as a custom image (chip_id 0x0010, stored as tasmota32h2.bin), offered and updated like any other chip; policy rules can name the family. Before, an H2 showed "The chip family is not known yet" and was never updated (#470, thanks @Noschvie)
Fixed
  • An ESP32-C3 that Tasmota names by its package, ESP8685 (embedded flash) or ESP8686 (QFN24), is recognised as ESP32-C3. Before, it showed "The chip family is not known yet" and was never updated (#470, thanks @Noschvie)
  • Settings → Notifications: "Send test" is dimmed and cannot be pressed while the switch of its channel (Webhook, E-mail) is off. It follows the switch on the page at once, before anything is saved, and a test posted with the switch off (no scripts) answers "switch the channel on to test it" instead of sending (#471).
Security
  • GHSA-fvgh-83hf-5jr9: a relay state containing a line break (a lone carriage return) could split the live event stream into extra events. The stream now treats every line break as part of one event, and relay states with control characters are dropped.
  • GHSA-cpfg-6c57-xg33: the confirmation of a retry that sends a destructive command again was not tied to its transaction: a token issued for another transaction or another confirmation was accepted. It now confirms only the transaction whose retry page issued it, and a retry no longer starts when the transaction cannot be read.
  • GHSA-fmj9-3vh6-rq53: cancelling a single job skipped the admin-only check for GPIO template and unknown job kinds when the job could not be read from the database. The cancel now fails when the job cannot be read.
  • GHSA-wq89-cqcx-3h3h: an open network scan stream kept sending scan results to an admin who had been demoted meanwhile. It ends at the next session re-check, like the firmware download stream.

0.27.0

“Cordouan”
Upgrade notes
  • No database migration (the schema stays at 0041). Nothing to change in the template.
  • Navigation moved (#410): Devices → List · Policy · Add; the four add-devices tabs are one Add page with four boxes; the database snapshots moved from Settings → Backups to System → Snapshots. The old pages GET /discovery and GET /import/tasmoadmin are gone without redirects (the Add page is /devices/new), so update bookmarks.
  • New: GPIO templates tab on the Policy page (#327). Only an admin can apply a template; every device gets a backup first ("Before GPIO template"), is changed one at a time and restarts. Operators see the tab read-only.
  • The Template command may now be 512 bytes (every other command stays at 255).
  • Security fix: cancelling and retrying template transactions, and job kinds this build does not know, is admin-only now.
Added
  • GPIO templates: apply transaction and service (part of #327): TasmoFleet can now apply a reference GPIO template to devices of the same template name and chip family as one transaction (job kind template, never an update): a blocking backup labelled "Before GPIO template", a fresh read of Module and Template that must still equal the previewed layout, one Template {json} command built from the parsed fields, and a read-back that waits for a proven restart (a larger BootCount and an uptime shorter than the time since the send, like restore). One device at a time; after the first failure the remaining devices are cancelled with "Nothing was sent". A failed template transaction is not retried (the page makes a fresh preview instead). The Template command may be 512 bytes instead of 255 (a classic ESP32 template is 250-300 bytes; Tasmota accepts less than 1200); every other command keeps 255. Devices reached over MQTT only can be read, changed and verified: Tasmota answers a Template read and a successful Template write with the template itself (no Template key), so the reply is matched by its shape (NAME and a GPIO array); a refused write answers {"Template":"Invalid JSON or Bad Chip Type"}, which fails the job at once as "nothing was applied". The command may only carry the keys NAME, ARCH, GPIO, FLAG and BASE (no CMND)
  • Home Assistant app, phase 1 follow-up (#442): a scripted run of the app in a real Supervisor (tools/ha-app-devcontainer-test.sh, workflow ha-app, nightly and on demand, never on pull requests) installs the app in the Home Assistant devcontainer, starts it, checks /readyz and the backup/restore round trip. tasmofleet-devenv has a new flag -allow-download-host so simulated devices on a non-loopback address can download from a firmware server that is not on loopback (development only)
  • Home Assistant app, phase 1 (#442, part of #209): deploy/homeassistant/tasmofleet/ holds the app (config, documentation, icon) with the web interface on 8080/tcp (HTTPS, self-signed certificate, local sign-in) and the firmware server on 80/tcp mapped to host port 8081. It is experimental and not published yet; there is no ingress, no Home Assistant sign-in and no automatic MQTT setup until the later phases. Inside an app (SUPERVISOR_TOKEN set) TasmoFleet reads the Supervisor's /data/options.json (option initial_admin_password) and logs plain text; a missing or malformed options file stops the start. Docker and Unraid installs are unchanged. The Dockerfile has a new target ha (root user and Home Assistant labels) that CI builds and smoke-tests; the default image is the same as before
  • GPIO templates tab on the Policy page (#327): devices are grouped by template name and chip family and compared with the most common template or one you choose (not stored); each group shows who matches, who differs (pin by pin) and who cannot be changed and why. An admin ticks the devices and opens a preview with the exact Template command, the restart and PowerOnState warning and the backup-first line, confirms with a tick, and a transaction applies it to one device at a time (backup, read again, send, verify) and stops the rest after the first failure. Operators see the tab read-only. A failed transaction is not retried: "Check again…" opens the tab for a fresh comparison
Changed
  • The version in the header, About and the startup log shows the commit of CI builds with 7 characters, like GitHub: 0.26.0+edge (41bfd05). The build stamp keeps its length: the startup log has a build field, tasmofleet version --full prints it, and image tags are unchanged (#436, thanks @Noschvie)
  • Devices → List · Policy · Add on one page, System → Snapshots (#410): the Devices menu now reads List, Policy, Add ("All devices" became "List", "Add devices" became "Add"; the heading of the list stays "Devices", its breadcrumb says Devices / List). Add is one page with four boxes instead of four tabs: Discovered and Network scan in the wide column, Add manually and Import from TasmoAdmin in the narrow one (phones: Discovered, Add manually, Network scan, Import). The Discovered table has no MAC, Hostname or First seen column any more (the phone card keeps the hostname), and its Online/Offline filter chips only appear while the list holds both. The Network scan box is the range, Scan now and one line about the last scan with its date and a link "All results"; the scan itself runs live on the results page. The database snapshot download, the stored snapshots, their schedule and the restore instructions moved from Settings → Backups to the new System → Snapshots page (right after Settings; admins), with the schedule saved by the page's own save bar; "Backups of deleted devices" now sits under the device backup schedule. Moved routes, no redirects: GET /discovery and GET /import/tasmoadmin are gone (the page is /devices/new); the scan results stay at /discovery/scan and /discovery/scan/{id}; Settings → Backups no longer holds the snapshots (/settings/system/snapshots); the old "Open Discovered" and bookmark addresses answer 404. Update bookmarks. No database migration (#440, thanks @Noschvie)
Fixed
  • The device list's pill row keeps its scroll position again after a Chip or Tag choice on a narrow screen: the current "All" pill was scrolled back into view and threw the row to its start (#428)
  • The device list's toolbar is one row again at 1280 px: the "No backup" pill made search, pills and Columns too wide for one line, so the pills dropped under the search box. The search field now shrinks to make room (#428)
Security
  • Cancelling and retrying transactions is admin-only for GPIO template transactions and for job kinds this build does not know; the role is checked before the job kind. Update, Make compliant, Send command and Back up transactions stay open to operators as before

0.26.1

“Skerryvore”
Upgrade notes
  • No database migration (the schema stays at 0041). Nothing to change in the template.
  • The device Overview has a new layout (#421, #420, #422), and devices identified before 0.22.0 now get their Status 2 facts (core, SDK, CPU frequency, build date) read automatically after the update (#417), at most one read per two seconds.
Changed
  • The device Overview arranges its cards in two column stacks (#421): Stability, Connection · Wi-Fi and "How TasmoFleet reaches this device" on the left, Identity and Activity on the right. Stability and Activity no longer stretch over the full width with mostly empty space; phones keep the order Stability, Identity, Connection, reach paths, Activity
Fixed
  • The Identity card no longer says "Most of Status 2 is not read yet" for good on devices identified before 0.22.0 (#417): devices that were probed before the Status 2 facts (core, SDK, CPU frequency, build date) were stored now get them read automatically, over HTTP while it works and over MQTT when it does not. One Status 2 per two seconds on the whole process, at most one per device per ten minutes (a failed read waits as long), only while the device is online. It is a read and writes only those facts; the identity fields stay as the last probe left them. No database migration
  • The Overview Reachability tile no longer says "Reachable" for HTTP days after one successful health poll (#416): it reads the same HTTP state as the card "How TasmoFleet reaches this device" (reachable within 15 minutes, not reachable, or not checked lately) and shows how long ago HTTP last answered. No database migration
  • The Reachability tile on the device Overview no longer runs past its edge on a phone: "MQTT · HTTP" fits the half-width tile at 320 px (#420)
  • Stability's recent restarts show the date for restarts that are not from today ("Oct 28, 12:31 PM"); only today's keep the bare clock time (#422)

0.26.0

“Skerryvore”
Upgrade notes
  • No database migration (the schema stays at 0041). Nothing to change in the template.
  • Restore over HTTP now works on official Tasmota builds (it did not before, see Fixed). It no longer needs the firmware listener or FIRMWARE_BASE_URL.
Added
  • Backups include the flash files of an ESP32 (and any build with a file system and firmware 14.6.0 or newer), part of #324: each backup run stores one more file, <stamp>_<host>.files.tar, with Berry scripts, folders and tlskey (1 MiB per file, 4 MiB and 100 files per run; bigger files are listed in the archive's manifest, not stored). The archive is a secret like the .dmp. A device without a file system, with older firmware or reachable over MQTT only gets a note instead. Retention, Keep and delete treat the archive as part of its run. No database migration; the backup pages and restore follow in later changes
  • The Backups tab shows the files of each run (part of #324): a run with an archive has a "Files" fold with the stored files and sizes, "Changed" / "New" against the newest earlier run that has files, "Private key" on tlskey, the skipped files with the reason, and for admins "Download files (.tar)" (audited like the .dmp download). Contents are never shown. A run without files says why in its detail line (no file system, firmware older than 14.6.0, taken over MQTT); a damaged archive shows "archive damaged" in the fold instead of failing the page
  • Restore and Replace device put the backup's files back (part of #324): when the backup holds a files archive, the files are written to the device first (read the current file, write, check the size), then the settings, with one restart. Nothing on the device is deleted. The restore pages show the file list before you confirm and a new step "Write the files" with the state of every file; the finished job states the number of flash writes (every upload is one on the device). A file is overwritten only when the backup taken just before holds its current content (so the restore can be undone); otherwise it is skipped and said so. A failed upload puts the old file back; a restore over MQTT, a device without a file system or firmware older than 14.6.0 restores the settings only and says so on the confirmation page. On a fresh replacement board, files in folders it does not have are skipped with the reason. The restore confirmation and progress pages now use the full content width in two card stacks. Verification compares the files with the backup. No database migration
  • Groundwork for backing up ESP32 files (part of #324): the device client can list, download and upload files of the flash file system (UfsList, GET /ufsd, POST /ufsu), and the simulator models that file system from live captures.
  • The "Devices without a backup" tile on Settings, Backups now opens the device list narrowed to exactly those devices. The list has a new "No backup" chip (?chip=nobackup) with the tile's count, so the two always agree. On a phone the active filter chip is scrolled into view in its row, so a filtered list never looks unfiltered (#381, thanks @Noschvie)
Changed
  • Small buttons (.btn-sm) and the small control height are one size now: .btn-sm, link buttons and the icon-sized small button use the --ctl-sm token (32 px on desktop, was 28 px for pagers and toolbars and a separate 32 px for .btn-sm), so they can no longer drift apart. On phones all of them are 40 px, the touch size (#329)
Fixed
  • Restore over HTTP did not work before this release and works now. Up to 0.25.0 a restore or replace over HTTP asked the device to fetch the backup itself with WebGetConfig, a command that no official Tasmota build contains: the job failed at the upload step ("WebGetConfig was not accepted: tasmota: unknown command") and the device stayed unchanged. Only the restore over MQTT worked. TasmoFleet now uses the device's own "Restore configuration" upload (GET /rs, then the backup as a multipart POST /u2), checked against the Tasmota 16.0.0 source and live on an ESP32. A restore therefore no longer needs the firmware listener or FIRMWARE_BASE_URL, and a device that cannot reach the firmware server is restored over HTTP too. A device whose restore page is missing (the minimal firmware) is refused before anything is sent (#430)
  • First-run setup works over plain HTTP again (#429): submitting the account form on http://<ip>:8080/setup/account in Chrome failed with 403 "cross-origin request rejected". The setup pages sent Referrer-Policy: no-referrer, so the browser sent Origin: null and, without Sec-Fetch-Site on a non-secure origin, the CSRF check rightly refused it. They now send Referrer-Policy: origin: the browser sends the real origin, and the setup token in the URL still appears in no Referer header at all (only scheme://host is sent). Security note: the CSRF check is unchanged; Origin: null and foreign origins are still rejected
  • The Unraid Community Applications page no longer shows two broken screenshots: docs/media/transaction.png and docs/media/device-dark.png, removed by the 0.25.0 media refresh but still linked by the 0.24.0 template the CA feed carries, are back. From the next release on the template links tag-pinned screenshot URLs, and CLAUDE.md now forbids deleting media a published template revision links (#423)
  • The status bar no longer reads a partly failed scheduled backup run as "Backup failed". A run that backed up some devices says "Backup · 19 of 27 backed up · 8 failed" in the warning colour, red "Backup failed" is kept for a run that backed up no device, and an all-good run shows "Backup · 27 of 27 backed up" in the normal colour. The phone sheet uses the same counts (#406)
  • Sorting by a column ascending no longer looks descending: the sort arrow in table headers was drawn the wrong way round (an ascending column showed the down arrow), and the audit log's Action column sorted by the stored key instead of the label it shows, so the order could look scrambled. The arrow now points up for ascending and down for descending, and Action sorts by the shown label (#407)
Removed
  • The firmware listener no longer serves /restore/<token>, and the restore no longer records "reaches the firmware server" when a device fetched its backup: the device-pull restore (WebGetConfig with a one-time token) is gone (#430)
  • The details.m-fold stylesheet rules and the e2e helper openFolds: no page has used the phone-only fold since the design v2 port (#329)
  • GET /login/local no longer redirects to /login; it is not a route any more (the sign-in form still posts there). Sign in at /login (#329)
  • The read-only "rules come from POLICY_RULES" mode of the policy pages: the rules service has always been present, so the mode could never be reached. A TASMOFLEET_POLICY_RULES value is still imported once as the first managed version, as before (#329)
  • docs/security/admin-listener.md, the record of the admin listener removed on 2026-10-19 (#329)

0.25.0

“Cape Point”
Upgrade notes
  • Database migration 0041 adds two tables for the stability history (restarts and hourly Wi-Fi samples). Nothing to change in the template.
Added
  • Stability per device: a new "Stability" card on the device Overview (between Relays and Identity) says Stable, Boot loop or Weak Wi-Fi. It shows the last restart in plain words (Tasmota's RestartReason, ESP8266 and ESP32 texts mapped by keyword, an unknown text shown as it came), restarts in 1 h, 24 h and 7 days counted from BootCount (so restarts TasmoFleet did not see are included), the Wi-Fi signal with a 24 h sparkline, reconnects since boot (LinkCount, Downtime) and the last five restarts. A boot loop is 3 or more restarts in 1 h or 6 in 24 h; weak Wi-Fi is an average signal below -75 dBm or 10 or more reconnects since boot. The device list shows a "Boot loop" / "Weak Wi-Fi" badge next to Online, and the dashboard's "Needs attention" lists both. Restarts come from the device's own tele/INFO3 over MQTT, or, on a device TasmoFleet reaches over HTTP, from an uptime that dropped between two polls followed by one Status 1 (never Status 0). TasmoFleet keeps the newest 50 restarts per device and Wi-Fi samples for 7 days; restarts that TasmoFleet causes itself (an update or restore, the Restart button, a bulk or console restart) do not count towards a boot loop. Database migration 0041 adds the two tables (#325)
  • The firmware cache now prunes itself. A new setting, Settings, Firmware & updates, "Releases kept" (FIRMWARE_KEEP_RELEASES, default 3, 0 = never delete, applies at once), keeps the files of the newest cached releases. After every successful index refresh and in the daily clean-up older releases are deleted, except files a queued or running update needs (a release with one such file stays whole) and uploaded firmware. Every run that deletes something is in the audit log as "Prune firmware cache" with the number of files and the bytes freed (#252)
  • Notifications by webhook or e-mail (#249): Settings has a new "Notifications" tab. Pick the events (an update transaction finished, with "any result" or "failed or partial only"; a scheduled device backup run with failures; a failed database snapshot), then a webhook (generic JSON, or the same JSON for a Home Assistant webhook trigger; optional secret header) and/or SMTP e-mail (STARTTLS, TLS or none). "Send test" per channel tests the form without saving, a status line shows the last delivery or test. The header value and the SMTP password are write-only and stored encrypted like the other secrets and only go to the address they were entered for (changing any part of the webhook address, or the mail server, port, security or user asks for them again; "Send test" leaves a stored secret out when the target in the form differs). The last delivery and test are kept in memory only: they are empty after a restart. The new settings are NOTIFY_* keys (see docs/configuration.md); like all settings they belong on the page, not in the environment
Changed
  • On a short page the strip right of the top bar and the status bar (the scrollbar's reserved room) is the bars' colour instead of the page colour, so the bars no longer look as if they end about 15 px before the window edge. The status bar group also has a divider before its first and after its last item, aligned to the content column (#395)
  • Card grids no longer line cards up in rows: in every grid the cards stack directly under each other per column, so a short card next to a tall one leaves no empty gap below it. The System Settings tabs (Notifications included), About, the Dashboard, the device Overview and the device Settings tab share one column-stack component; the reading order on a phone is unchanged. The rule is in the design system (docs/spec/design-system.md, "Card stacks") (#391)
  • Fresh installs and upgrades start pruning at once: with more than three cached releases the older ones are deleted at the first clean-up after the update. Set "Releases kept" to 0 to keep everything as before (#252)
  • Refreshed screenshots in the README and the Community Apps entry: the dashboard, the device list, the device Settings tab and the policy rule editor with catalogue suggestions, from the simulator (#397)
  • A firmware command typed in the console or sent as a bulk command (Upgrade 1, OtaUrl) now holds the device for TasmoFleet updates until it has restarted (LWT offline then online, or an uptime that shows a new boot) or 10 minutes have passed: the update preview marks the device as refused and says that the typed update is probably still running, and a submit or retry for it is refused. Before, the command's job ended as soon as the device answered, so an update started in the following minute of flashing could overlap it. The hold is kept in memory only; a TasmoFleet restart forgets it (#347)
Fixed
  • System, Settings no longer answers 500 when <data>/snapshots is a file instead of a folder (#399): the page renders and the "TasmoFleet database" card says the snapshot folder is not a directory; scheduled snapshots fail with that reason until it is fixed
  • Settings, Backups, "Download snapshot": after the password or identity-provider confirmation the page comes back with "Include secret.key" still ticked and the Download button focused, so one key press finishes the download. Before, the box was unticked again and the person had to tick and click a second time (#387)

0.24.0

“Hook Head”
Upgrade notes
  • Database migration 0040 adds a nullable column to principals (the chosen display name). Nothing to change in the template.
Added
  • Display name for SSO and proxy-header accounts: single sign-on now shows the identity provider's full name (the name claim, configurable as OIDC_NAME_CLAIM; for a proxy the existing name header) instead of the user name, at every sign-in. On System → Account every user can override it with a name of their own, shown in the header menu, the avatar, the Users page and as the label of the audit user. The override is kept at the next sign-in, and clearing it brings back the provider's name, then the user name. The identity does not change: audit rows keep naming the stable subject, a name equal to another person's or imitating a system actor or a local account gets the subject suffix, and zero-width, bidirectional and other invisible characters are refused in names. Database migration 0040 adds one nullable column (#283)
  • TasmoFleet backs up itself: Settings, Backups, "TasmoFleet database" downloads a consistent snapshot of the database (SQLite VACUUM INTO, made while it runs; admins only, after a password confirmation or a sign-in at the identity provider, a proxy-header sign-in ticks a checkbox; every download is audited). Optionally the download is a .tar with secret.key, which the encrypted values in the file need. A new "Snapshot schedule" card keeps scheduled snapshots in /data/snapshots (SNAPSHOT_INTERVAL, default off, at least 1h; SNAPSHOT_KEEP, default 7) with Download and Delete per file and the last result, including why a run failed. Restoring stays "stop, replace the file, start" (README, "Backing up TasmoFleet") (#301)
Changed
  • The Backups tab's "Schedule & retention" card is now called "Device backup schedule", next to the new snapshot schedule (#301)
  • Device settings tab: the fields sit in grouped cards (Names, Options, Network & telemetry, MQTT, Power & flash, Time) in three independent columns from about 1200 px, two balanced columns below and one on a phone; each card has a one-line note, the Save bar spans the full width and stays in view. SetOption0 moved from Options to Power & flash, next to the other flash-write settings (#367)
  • Add devices → Network scan starts with the ranges of your device networks already in the field, ready to scan. A range a scan refuses (larger than /22) is not prefilled; a note names it and the limit. The field is wider so two ranges fit (#270)
Fixed
  • The Columns menu now reliably reopens after you pin a column, and stays open when a device event refreshes the list at the same moment. The page that was about to reload could take the reopen note itself, and a refresh right after the menu opened could drop its open state (#273)
Security
  • Single sign-on and proxy-header identities whose subject starts with local: are refused at sign-in (403 with a clear message, audit entry denied: reserved subject). That prefix is the subject of local accounts, and the audit log, the Users page and the user directory tell local accounts by it, so such an identity could appear as the local admin and share its audit rows. Roles were never affected (#380)

0.23.1

“Cape Hatteras”
Added
  • Container image for linux/arm64 (Raspberry Pi 4/5) next to linux/amd64: every tag is now a multi-arch image. The arm64 image carries the backup decoder as well (#354, thanks @t4nki)
  • Backup decoder in the arm64 image: decode-config is an unofficial aarch64 build of exactly upstream v16.0.0 plus the merged workflow change tasmota/decode-config#127 (LGPL-3.0, built in nofuturekid/tasmota-decode-config), so System → About shows it as available and backups get the decoded JSON view and the comparison on arm64 too. The Dockerfile pins its SHA-256 and goes back to upstream's asset once upstream releases aarch64. The image smoke check decodes a backup under QEMU (#211)
Changed
  • Examples, placeholders and help texts use common home-network addresses (devices 192.168.1.0/24, container 192.168.1.10, proxy 192.168.1.2) instead of homelab-shaped ones, and the Unraid template in deploy/unraid/ no longer prefills the container IP or the trusted proxies (#368)
Fixed
  • Device settings, Options: the help line of SetOption19 (and SetOption4) no longer repeats the description in the label; each fact is shown once (#363).
  • Settings: the yellow "unsaved" dots (on the tab and in the save bar) no longer move their neighbours when they appear or disappear; their space is reserved, so tabs, text and buttons stay where they are (#362).
Security
  • Policy rules can no longer name commands that act when sent without a value (RC522Reset, ClientReset, SSPMReset, TRVReset, WieAllReset, DisplayClear, I2CScan, Scd40Strt and others: names ending in Reset, Clear, Erase, Restart, Scan, Start, Stop, Init, Test, Save and the like, plus a verified list). Reading such a rule used to reset a peripheral on every policy check. Stored rules that name one are left out and shown as invalid (#358).

0.23.0

“Cape Hatteras”
Upgrade notes
  • Database migration 0039 adds a nullable column to devices (the remembered ESP32-S2 build). Nothing to change in the template.
Added
  • Status bar fixed to the bottom of every page: browser live state, MQTT broker (host and port, since when it is down), running jobs with progress and a link (two named, then "+N more"), the last scheduled backup run (time, OK or how many devices failed) and the version. On a phone it is one collapsed line that opens into a sheet. It refreshes from the existing event stream plus a new broker event; nothing polls, and no credentials or error texts are shown. The last backup run is kept in memory, so it is empty after a restart until the next scheduled run ends (#307)
  • Console and bulk "Send command": Arrow Up and Arrow Down walk through the last 50 commands of this browser (newest first, duplicates collapsed, shared by every device's console), Escape brings back what you had typed, and "Clear history" empties it. Commands with a credential and commands the policy refused are never kept (#321).
  • Policy rule editor: the command field learns from the Tasmota command catalogue (#268) instead of a hand-written list. Suggestions come from the Tasmota versions present in the fleet (one catalogue is pinned per version: 12.5.0, 13.4.0, 14.4.1, 15.6.0, 16.0.0), indexed commands such as SetOption<n> offer their index range as a number box, the value field follows the command's kind (number with the range the firmware checks, decimals where it answers one, on/off, text), and a short description with a link to the Tasmota docs is shown (only links to anchors that exist, checked against a pinned list). A check warns when a command does not exist in some Tasmota versions of the fleet, when a build lacks the feature it needs or when an index is out of range, and the devices card says why a device is "Not judged"; saving is refused only for a command that exists in no Tasmota version present in the fleet (#269)
  • Update preview: the ESP32-S2 build (s2 or s2cdc) you chose for a device is remembered and preselected next time, shown as "Build s2cdc, remembered from your last update" with Change (ask again) and Forget (clear), and the firmware safety check still verifies the build (#186)
Changed
  • System → About: the page uses the full content width; the version card is a band with only "What's new" (the Styleguide button is gone, it has its own menu entry), the components are a 2×2 grid, and the effective configuration is always visible in two tables side by side with its values in monospace (#351)
  • Settings → Advanced: "Set by the container" and "Reset all settings" take the full width; the container settings are no longer behind a disclosure but sit always visible in two key/value tables side by side (one table on a phone) (#350)
  • Top bar: the version and the "Live" indicator moved into the new status bar at the bottom (the phone header loses its Live text too); the phone drawer keeps its version link (#307)
  • Console and bulk command: Upgrade and OtaUrl with an argument are refused while a TasmoFleet update job (queued or running) holds the device, with a clear message; the bulk preview names each refused device and the rest still goes (#240)
  • Policy rule form: "Applies to" is an always-open, full-width section after "When a device deviates": a summary line, chip families as toggle pills, tags as chips with an input and the tags in use with their device count, picked devices as chips above a search (name, IP, tag) over a fixed-height list, and Exceptions with the same names. Nothing on the page shifts while the scope is edited. The four tiles and the line under the title now follow the draft like the Devices card (#235)
  • Policy rule page: the tile "Applies to" and the line under the title count the exceptions listed in the rule (what the form shows), no longer only the devices an exception actually removed (#235)
Fixed
  • Stylesheet: a missing closing brace after the policy "Deviating devices" mobile rules made every rule after it (policy History tab spacing and indented folds, Identity card headings) apply only on narrow screens; the brace is back, and a unit test now fails when braces do not balance (#346)
  • Device page: the info text of the Status 2 field "CR" is below the 160-character tooltip limit (#346)

0.22.1

“Bell Rock”
Upgrade notes
  • No database migration, nothing to change in the template.
Breaking
  • Settings: the tab "Discovery scan" is now "Discovery" (?tab=discovery); the old ?tab=scan opens the first tab, no redirect (#335)
Changed
  • Settings, Discovery tab: the "MQTT discovery" card moved here from the MQTT tab and sits next to "Network scan", which links to the device networks it is limited to. The MQTT tab keeps Broker and Topics (#335)
  • Settings, MQTT: "Broker changed: keep the stored password for the new broker" sits under the Host/Port row and shows only while host or port differ from the stored value (always visible without script). Saving works as before (#333)
  • Settings: fields that depend on a switch are ordinary fields of their card, no indented block with a rule; they are still dimmed while the switch is off (#334)
  • Settings, Backups: the Interval is empty (placeholder 24h) while scheduled backups are off instead of showing "0s"; switching on proposes 24h; off still stores 0 (#336)
Fixed
  • Settings page brought back in line with the approved mockup (#338): Sign-in mode is a full-width card with the two choices side by side and the PUBLIC_URL note below (its Restart / confirm badges sit in its header, no repeated label or lone reset line), Identity provider spans two columns and Roles and claims the third; Firmware source spans two columns and two rows with Cross-check and Updates (OTA) stacked beside it; the card of deleted devices' backups is always shown (with "No backups of deleted devices."); "Set by the container" is a card with a disclosure on Advanced and the save bar sits below it; the TLS switch has its label; the MQTT discovery status is a flat strip with Open Discovered below the switch
  • Settings: "Reset to default" shows only when the value differs from the default; the "Restart" and "Asks you to confirm" badges stay once in each card header as in the approved mockup (#337, #338)
  • Settings: switching "Scheduled backups" on and off again leaves the tab without unsaved changes (the Interval takes back the proposed 24h; saving with the switch off is 0 whatever the field holds) (#337)
  • Settings, Backups: the Storage card was missing when the page was opened on another tab, because the storage facts were loaded only for ?tab=backups; they are now always loaded. The info icon of "Never prune younger than" stays next to its label (#336)

0.22.0

“Bell Rock”
Upgrade notes
  • Database migration 0038 adds one column for the raw Status 2 fields. Devices show them after the next identity refresh or probe.
Breaking
  • Settings: the tab addresses changed (?tab=network, mqtt, scan, backups, firmware, signin, advanced, general); the old discovery, devices, fwsource and metrics open the first tab, no redirects (#275)
Added
  • Device page, Identity card: a block with exactly the StatusFWR fields of Status 2 under Tasmota's names and with the values as the device sent them (Version, BuildDateTime, Core, SDK, CpuFrequency, Hardware, CR), each with a short explanation behind an info icon. What TasmoFleet works out itself (variant, language, self-compiled, flash size) is shown below it as "TasmoFleet's reading". The former "Hardware", "Build" and "Firmware" rows are replaced by these two blocks (#289, thanks @Noschvie)
  • The Identity card warns when the settings text area is nearly full ("Settings text area almost full (650/699): longer values such as OtaUrl may not be saved"), and the Make compliant preview warns when the text values of a plan would not fit any more. The threshold is the system setting CR_WARN_PERCENT (50 to 100, default 90) (#289, thanks @Noschvie)
  • The device header shows the hostname the device reported (#290, thanks @Noschvie)
Changed
  • Backups tab: each backup line shows the stored file name (as on disk, to check the backup output in the file system), and the Download button's hover help names it (#290, thanks @Noschvie)
  • The start log names the served TLS certificate (SHA-256 fingerprint and expiry), as the emergency runbook promises; the unused internal/bulk package is removed (#250)
  • Settings page reworked (#275): cards in a grid of up to three columns instead of one narrow card, eight tabs instead of ten (Metrics joins General, Devices joins Network & devices, Updates (OTA) joins Firmware & updates, MQTT discovery sits on the MQTT tab, the network scan is its own tab "Discovery scan"), one line of help per setting with the longer text behind an info button (click, Escape, works on touch), switches for every on/off setting, settings that depend on a switch are dimmed while it is off, "Reset to default" and the Restart / Environment badges sit in the field's label row, the firmware sources are checkboxes, the backup schedule has an on/off switch (off stores 0 = manual only), the danger zone and the container list sit below the form, and one sticky save bar lists the tabs with unsaved changes (edits survive a tab change; leaving with unsaved changes asks first). On a phone a section picker replaces the tab strip. breaking: the tab addresses changed (?tab=network, mqtt, scan, backups, firmware, signin, advanced, general; the old discovery, devices, fwsource and metrics open the first tab, no redirects)
  • CI: a release tag now runs a shorter pipeline (the fast checks, the @core e2e tests, and race tests of the safety packages); the full e2e, race and mutation runs stay in the nightly build and no longer hold up a release.
  • Overview: a discovered device that is offline no longer appears under "Needs attention"; it is listed only on the Discovered page, which now shows how long it has been offline (when known) next to its hostname, and the "New devices found" tile says how many of them are offline (#294, thanks @Noschvie).
Fixed
  • Device page: the Settings tab opens at once with a loading state and reads the device into it, instead of showing nothing until the device had answered; a failed read keeps its reason text. The Activity table is no longer redrawn on every live refresh when nothing in it changed, which made it flicker. The Console and Backups tabs read only local data and already opened at once (#308)
  • What's new: releases with a name in their heading (0.21.0 "Tower of Hercules" and later) were missing from the page; they are listed again, with their name
  • "Test discovery" on the Sign-in tab answered 400 because the form also carries the broker fields; it now accepts the whole form like the MQTT test (#275)

0.21.0

“Tower of Hercules”
Upgrade notes
  • No database migration, nothing to change in the template.
  • Older patch images of 0.11 to 0.19 are removed from ghcr.io and Docker Hub; pin to the newest patch of a minor or to latest.
Added
  • Release names: every minor release is named after a lighthouse (list in docs/release-names.md); the CHANGELOG heading may carry the name and the GitHub release title becomes TasmoFleet X.Y.Z "Name"
  • The registry cleanup keeps only the newest patch of each older minor version on ghcr and Docker Hub (latest, edge and every patch of the newest minor stay); the older patch images of 0.11 to 0.19 are removed, so pin to the newest patch of a minor or to latest (#300)
  • Config restore over MQTT (FileUpload Type 2) for devices on our broker, when HTTP fails or the device cannot reach the firmware listener: the safety backup, the identity check, the upload and the verification use the same path, the restart is proven by the boot count, and the restore page names the path and records it on the job. A restore over MQTT whose backup changes the device's topic, broker, user or password (or cannot be compared) needs an extra acceptance, because TasmoFleet could lose the device. FIRMWARE_BASE_URL is needed only for the HTTP path and Replace device (#258)
Breaking
  • Restore no longer needs FIRMWARE_BASE_URL for the MQTT path: the service starts without it and offers the HTTP restore (WebGetConfig) only with it; it is no longer switched off together with the firmware listener (#258)

0.20.0

Upgrade notes
  • Database migrations 0035, 0036 and 0037 run automatically; a .pre-N copy of the database is written before each (the newest three are kept).
  • Devices that TasmoFleet reaches over MQTT only need their MQTT password set per device (device edit form) for backups over MQTT.
  • After the upgrade, the background read of every device's group topic (one device per two seconds) may hold MQTT writes for a few minutes.
Breaking
  • Make compliant does not change Topic, FullTopic, MqttHost and the like on a device TasmoFleet reaches over MQTT only; the job reports them as skipped (use the console, which asks to confirm). Bulk commands that could cut such a device off are refused in the preview (Part of #256)
  • Console and bulk commands refuse FileDownload, FileUpload and Upload for every role (their payload carries the device's MQTT password). cmdpolicy.Prepare takes PrepareOpts
  • Audit details of console commands, settings writes and Make compliant gain "via" (Part of #256)
  • Over MQTT the console refuses commands that run other commands (Json, Publish, Publish2, Rule with a rule text, RuleTimer, WebSend, WebQuery, Br, Script): they could send Status 0. Over HTTP they work as before. A device reached over MQTT only asks for a confirmation before a change of Topic, FullTopic, GroupTopic, MqttHost, MqttPort, MqttUser, MqttPassword, MqttClient or SetOption3 0
  • An update of a device whose language build was never read from its web page (Tasmota shows it only there) is refused until a language is chosen; fwselect.Select returns ErrLanguageUnknown unless the choice names one ("EN" is allowed), and so does Guard. It applies only where a language build exists (not to ESP32-C3/S3/..., solo1 or the non-plain variants with an official image). Devices whose language an HTTP probe read are unaffected, including those stored before this release (migration 0037); only devices known from MQTT alone need the choice (Part of #259)
Added
  • Devices TasmoFleet cannot reach over HTTP are managed over MQTT when they use our broker: console, Send command, settings reads and writes, policy reads and Make compliant. HTTP stays first; MQTT takes over when HTTP is known to fail, or inside one call only when the request provably never left (connection refused, outside DEVICE_NETWORKS, SetOption128, 401). A timeout or a dropped connection is never followed by a second send on the other transport. The audit details of these writes say via http or mqtt. A background read of every device's GroupTopic (Status 1, one per two seconds) and the MQTT identity refresh keep the group-topic check of MQTT writes satisfied (Part of #256)
  • Firmware updates over MQTT (OtaUrl + Upgrade): a device TasmoFleet cannot reach over HTTP downloads the image from TasmoFleet's firmware server; staged ESP8266 updates included. The path is chosen once in the preview (the row says "Over MQTT" and why, or why there is no path), stored with the job and pins every call of it. The language build, which Tasmota shows only on its web page, is asked for in the preview for devices that cannot be read over HTTP. A job over MQTT keeps the identity stamp and the language fields; the device is backed up over MQTT first when a backup path exists; without one the preview refuses with the reason, or "Back up each device" can be switched off (Part of #259, #292)
  • Config backups over MQTT (FileDownload) for devices on our broker whose HTTP does not answer, also in scheduled and bulk backups; the device's MQTT password is stored per device on the edit form (not set, the same as TasmoFleet's broker password, or its own; write-only, an empty field keeps it, changing it or ticking Retry clears a "device refused it" mark). The bytes equal /dl, size and MD5 are checked, a wrong password (Error 1) is remembered and blocks further MQTT transfers until it is changed, and a backup that MQTT cannot carry says why (#257) (#45, thanks @Noschvie)
  • Device list: optional columns OTA URL and Group topic (hidden until chosen in Columns, sortable, long values cut off with the whole value on hover), each with the time it was read. The group topic comes from the same read-only Status 1 as the OTA URL and from the boot announcement tele/<topic>/INFO1, so no new device read. Values read before this release show no time until the next probe. Migration 0035 (#239)
  • The device page shows how TasmoFleet reaches the device (HTTP, MQTT, device MQTT password, firmware server, experimental upload over MQTT) and why an action is unavailable; disabled Back up, Restore, Update and the Settings and Console tabs carry the same sentence, and the device list marks devices managed over MQTT (#79)
  • fwselect.MQTTUploadFits: a fixed rule, no try-and-see, for whether an image fits a firmware upload over MQTT (FileUpload Type 1), from Status 4 Free. A known safeboot layout (one app partition) is refused. Nothing uses it yet (Part of #262)
  • Internal tooling: Tasmota command catalogue (internal/tasmotacmd), generated from the pinned Tasmota v16.0.0 source by make catalogue: which RESULT key answers which command, with commands whose key cannot be derived statically listed as loose (#268)
Changed
  • A device that cannot reach the firmware server (Failed HTTP error: connection failed on its UPGRADE report) fails the update at once with that reason instead of retrying with a second Upgrade 1, and is remembered as not reaching it (Part of #259)
  • Repository: removed the v1 design-system bundle (design/ds-bundle), its build scripts and the Claude Design sync notes; the app never used them. The v2 prototype and the fixture tools stay
  • Images: each push to main deletes the previous edge on ghcr.io and Docker Hub, so no untagged images pile up (releases are never deleted)
  • README: no longer lists notifications by webhook or e-mail; they were never built (#249)
  • Policy: Save rule ends the editor too: the overview names the saved rule and marks its row
  • Policy overview: shorter compliance bar, so rule names and scopes do not wrap
  • Images are built without provenance attestation: one manifest per build, so a push of edge no longer leaves three untagged entries behind on Docker Hub
  • Docs: checked against the code. User docs: the metrics setting name, the scan range limit, the operator role description, the build-from-source command, the Authentik refresh-token note, a certificate-fingerprint claim the server does not fulfil. Security notes: renamed test references, the legacy policy-rules variable, the address-change confirmation. Design system and fixtures: folds, breakpoints, simulator timeline, safeboot push. Spec and CLAUDE.md: route list, backup retention, settings that apply live, first-run text, and features that were planned but never built (notifications, development channel, firmware cache pruning)
Fixed
  • The toast after "Back up now" in the device list showed umlauts and other non-ASCII characters garbled (the header was sent as raw UTF-8, which browsers read as Latin-1) (#288, thanks @Noschvie)
  • Device list: switching a relay of an HTTP-only device could flip back to its old state when a health poll that read the device just before the toggle finished just after it, and likewise for a state reported over MQTT; the newer state now wins over that poll (#206)
  • CI: module downloads fall back to a direct fetch when proxy.golang.org fails, which once failed a full mutation shard (#225); the flaky e2e check of #237 is fixed in the test, and the one of #218 waits for the rows to have a box before it measures (a mitigation, the cause is not proven)
  • Policy: Cancel in the rule editor goes back to the policy overview (it reloaded the rule page, or opened the template picker for a new rule)
  • Policy: Duplicate opens an unsaved copy; nothing is stored until Save rule
Security
  • The device MQTT password is stored encrypted like the web password, never rendered, logged or audited, and is sent only to that device's topic (#257)
  • MQTT command channel: closing it while a command waits for its answer now reports an unknown outcome (the command was published), not "nothing was sent"; a closed channel never publishes
  • Command policy: the confirmation for Status 0 (and Reset and other guarded commands) could be bypassed by a Backlog part spelled with a leading Backlog (Backlog BacklogStatus 0), which Tasmota strips before running it; every part is now judged after the same stripping

0.19.2

Security
  • golang.org/x/net 0.62.0 (GO-2026-6860, GO-2026-6866 in the HTML parser; govulncheck finds no call path from TasmoFleet, reported by Docker Scout)

0.19.1

Changed
  • Backups: the manual backup cooldown is 1 minute and ends when the last backup of a device is deleted (#208, thanks @Noschvie)

0.19.0

Upgrade notes
  • No database migration, no change to ports, variables or the Unraid template.
  • Policy rules get a stable id on the first save. After that, 0.18 can no longer read the saved policy rules: to downgrade, restore the database copy taken before the upgrade.
  • Old policy links (/policy?edit=N, ?new=1, ?v=ID) open the overview; each rule has its own page now (/policy/rules/{id}).
  • The image is also on Docker Hub as nofuturekid/tasmofleet (same tags and digest as ghcr.io).
Added
  • Policy: Deviating devices tab (/policy/deviations) with the deviations grouped by device, per-row Make compliant (admins tick the rows; Required rows start ticked) and Back up for devices that cannot be judged yet (#197)
  • Advisory deviations can be selected by hand for Make compliant (never preselected), on the Deviating devices tab, in the overview dialog and in the device tile's preview (#197)
  • Images are also published to Docker Hub (nofuturekid/tasmofleet) under the same tags as on ghcr.io (#165)
  • Policy rules have a page of their own, /policy/rules/{id}: four tiles (applies to, match, deviate, not judged), the editor, and the devices the rule judges with the value each has and where it came from (live or backup); the table updates as you edit without reading any device. Operators see the same page read-only (#197)
  • New rule page (/policy/rules/new): start from a template, from Any command (refused commands show the reason) or with Copy from a device (a fresh live read of that one command; backup value when the device does not answer, with the source shown). Each opens an unsaved draft; nothing is stored before Save rule (#197)
  • Policy rule templates for NTP server, Latitude, Longitude, OTA URL (ESP8266 and ESP32), Retain power state, Status LED and Dynamic sleep, next to the existing ones; each was checked against the Tasmota 16 source (reply format, accepted values, restart behaviour), see docs/research/tasmota-policy-templates.md (#197)
Changed
  • The policy page is reworked: five tiles (rules, compliant devices, deviating, not judged, live check), a rule table with a compliance bar per rule, a banner for devices that break a Required rule, and an own page per rule. Rules are reordered by dragging the handle or with Move up and Move down; a disabled rule is dimmed and not checked (#197)
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • Policy history is its own tab, /policy/history: versions grouped by day, quick saves of one person within 2 minutes folded into one entry (expandable), each entry says what changed, and a rule page lists only the versions that touched it. Reverting lands on the tab (#197)
  • Policy rules have a stable id that survives edits, reordering and export/import; rules saved before this version get r1, r2, ... by position on the first save (#197)
Fixed
  • Saving or deleting a policy rule no longer overwrites a change someone else saved in the meantime (#222)
  • One stored policy rule that no longer passes validation (a value from an older, laxer import) no longer switches every rule off: it is flagged on the Policy page ("invalid: …") and left out of the counts, and the other rules keep working (#197)
  • Backups: when "Back up now" is refused by the 5-minute cooldown, the message says when the last backup was taken ("Last backup 4 min 40 s ago; a new one is possible in 20 s"), also after that backup was deleted (#208, thanks @Noschvie)
  • Downloaded backups keep umlauts and other letters of the device name (Küche-....dmp): the header carries an ASCII fallback and the RFC 5987 filename*, and a hostile name cannot inject headers (#208, thanks @Noschvie)
  • The Download and Delete buttons of a backup row have their borders again, like the other buttons (#208, thanks @Noschvie)
  • Backup files left in old numeric folders that no backup entry references are now reported at start and listed under Settings → Backups instead of being silently ignored (#205, thanks @Noschvie)
  • Latitude and Longitude rules could never match: a device stores them as float32 and reads back 40.119999 for 40.12. Readings and the rule's expected value are now compared at four decimals (11 m), whatever the admin typed (52.52, 52.520008); a value that is not a number in range is refused (#197)
  • The simulator accepted LedState 8, which Tasmota refuses (#197)
  • The backup layout migration also moves the kept backups of deleted devices; backups whose files were deleted by hand are shown as missing and can be removed (#205, thanks @Noschvie)
  • Make compliant now knows which SetOptions restart the device (3, 15, 37, 55 when set to 0, 68, 69, 75, 85, 88, 97, 102 to 104, 106, 107, 115 when set to 0, 132, 151 to 153): they run last and the restart is not reported as an error (#196)
Breaking
  • Templates no longer save a rule at once: a template opens a draft on the New rule page. POST /policy/rules/template is removed (#197)
  • /policy?edit=N, /policy?new=1 and /policy?v=ID no longer open the editor or a diff; the overview is shown and the parameter is ignored. The rule editor is the rule page, /policy/rules/{id} (#197)
  • The rule forms name a rule by id instead of the list position idx (duplicate, enable, disable, move, delete and the delete confirmation /policy/rules/delete?id=); idx is refused (#197)
  • The policy rules export (and POLICY_RULES format) gains an optional id per rule; exports now include it, and an import with a duplicate or malformed id is refused (#197)

0.18.0

Upgrade notes
  • Backups move to a new layout on the first start, automatically. Files go from backups/<device-id>/ to backups/<hostname>/<stamp>_<hostname>.*; each file is moved only after its SHA-256 matches the index, nothing is deleted, and a downgrade to 0.17 still reads them. Only scripts that read the data directory directly must follow the new names. See Upgrading to 0.18.
  • Migrations 0032 to 0034 run automatically. No change to ports, variables or the Unraid template.
Added
  • Add devices, Discovered: filter the list by state (All, Online, Offline; Offline is the device's MQTT last will) and sort it by clicking the column headers (#130, thanks @Noschvie)
  • Update an ESP32-S2 by choosing its build (#88). The two S2 builds (s2 and s2cdc) cannot be told apart from the device, so an S2 was always refused. The update preview now lists it as "Choose the build" with one line on what each build is for; nothing is preselected, and without a choice the refusal stays. The choice is per update and kept on the job (visible in the update and transaction views, copied by a retry, kept across a restart). A build that does not belong to the chip, or contradicts a known chip revision, is refused. Not tested on a real ESP32-S2 yet. Migration 0033 adds the job column.
  • Policy rules can check any Tasmota command that answers a plain query (Sleep, LedState, ...), not only the settings in the list. The rule editor has a free command field with the known settings as suggestions. Status in any spelling, secrets, commands that act when sent bare and commands that need a confirmation in the console are refused, with the reason. Make compliant sets such a rule like any other, admin only (#36).
  • Push upload into safeboot (#30). An ESP32 with the safeboot layout (one app partition, such as the QuinLED boards) cannot take a full image while its firmware runs, so "push where possible" used to download there. It now does what the web UI does: restart the device into safeboot once, upload the image there, wait for the new version. Every step is guarded (Guard before the restart and again right before the upload), the restart is never sent twice, a lost reply is probed, never answered with a second upload, and a job that ends with the device in safeboot is sent back once (probe first, one request, at most one recovery); if that fails, the job says where the device is and what to do by hand. Tested on a real ESP32 (16.0.0 reinstall, and cancel with switch back). Migration 0034 adds two job columns.
Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • Backups are named after the device (#129, thanks @Noschvie), breaking for scripts that read the data directory. Files now live in backups/<hostname>/ and are called <stamp>_<hostname>.dmp (.snapshot.json, .decoded.json) instead of backups/<device-id>/<stamp>-<kind>.<ext>. Existing backups are moved automatically on the first start, each only after its SHA-256 matches the index, and nothing is deleted; there is nothing to do. A downgrade to 0.17 still reads them (the index stores the path). Scripts that read backups/<id>/ must follow the new names. See Upgrading to 0.18.
  • Add devices, Discovered: the never-filled Model and Firmware columns are gone and "Name / hostname" is split into "Device name" and "Hostname". The default order is now last seen, newest first (#130, thanks @Noschvie)
Fixed
  • Device list: the live cells of the phone cards no longer each trigger a full refit of the table, which kept the page busy for tens of seconds to minutes while a large fleet came online
  • Add devices, Discovered: the Source column is wide enough for "MQTT discovery" in every font
  • Policy rule editor: SetOption32 to SetOption49 take a number (1 to 250 for SetOption32, 33 and 45, otherwise 0 to 255), not an on/off toggle. They are numeric parameters in Tasmota (e.g. SetOption45, bistable relay pulse time), and an out-of-range value is refused
  • An ESP32 with the single-app (safeboot) layout never gets the plain upload into its running app, even when Status 4 Free is larger than the image or its upload page could not be read when planning: a push goes through safeboot (or downloads while that is off), and the upload itself refuses such a page (#191)
  • An OtaUrl whose password contains /, # or ? could be shown or compared in part. The URL credential redaction now parses the URL, and a value that cannot be cleaned safely is shown as *** (#168).
  • A device that went offline while tasmofleet was starting could stay without its Offline state: the broker clears the retained flag on an LWT it delivers live, so the replay for later subscribers missed it. Every LWT is now kept for replay, and a replay can no longer be overtaken by a live message (#161).
  • A correct ESP32-S2 or ESP32-P4 update no longer ends as "not taken": these chips report the shared image name afterwards, so the check now compares with what the device can report.
  • Policy rules for Sleep compared the whole reply and never matched a real device: Tasmota answers {"Sleep":{"50":{"Active":"100"}}}. The rule now compares the configured value (50), so Make compliant no longer resends it forever. SerialLog and SysLog answer in the same format and are read the same way.
Security
  • Dependency: go-jose updated from 4.1.4 to 4.1.6 (#178).
  • Passwords in device URLs (OtaUrl) no longer leak through the policy report when they contain /, #, ? or @ (#168).

0.17.0

The container port of the UI is now always 8080 (it was 8443 in the zero-configuration TLS mode), and TASMOFLEET_TLS_SELF_SIGNED no longer exists. Full steps for Unraid, docker run and Compose, and what happens if you do nothing: Upgrading to 0.17.

Upgrade notes: action may be required
  • Unraid (Community Applications) and installs with an explicit `TASMOFLEET_LISTEN` (`:8443` or `:8080`) keep working without any change (unless the UI listen row was deleted, see the next point). A saved :8443 is still honoured, and the scheme of the shipped default template (self-signed TLS) and of the proxy variant (plain HTTP) is unchanged. On br0 or a custom IP there is no port mapping: the container listens on the saved UI listen port (8443 with the template defaults, 8080 in the proxy setup). If you delete UI listen, it listens on 8080 (UI) and 80 (firmware) of its IP, so update bookmarks and the reverse proxy upstream. The start log warns if TASMOFLEET_TLS_SELF_SIGNED is still set; delete it.
  • Any container with an `8443:8443` mapping and no (or an empty) `TASMOFLEET_LISTEN` -- `docker run`, Compose, or an Unraid container whose UI listen row was deleted from its template: the UI is unreachable on the old port. An empty value counts as unset (since 0.16.1), which gave :8443 before and gives :8080 now. Change the container port to 8080 (-p 8443:8080, Compose "8443:8080"). An Unraid container whose UI listen field is merely emptied keeps working: Unraid falls back to the template default :8443.
  • `TASMOFLEET_TLS_SELF_SIGNED=true` together with `TASMOFLEET_TRUSTED_PROXIES` (or `ALLOW_INSECURE_HTTP=true`) now serves plain HTTP (before: TLS). Point the proxy at http://.
  • Outside a container the zero-configuration TLS listener moves from :8443 to :8080; set TASMOFLEET_LISTEN=:8443 to keep the old port.
Added
  • docs/configuration.md: every environment variable in one place (purpose, default, kind bootstrap/lock/legacy, secret and _FILE), how TLS is chosen, the time zone (TZ), linked from the README and the Unraid templates; a test fails when the code and the document drift apart (#153).
  • docs/upgrading.md: upgrading to 0.17 step by step (#152).
  • Sensor telemetry: the tracker now keeps the numeric readings of every tele/SENSOR block besides ENERGY (BME280, BH1750, DS18B20, ...) with their units (TempUnit/PressureUnit), bounded in count and name length, for the device page's Sensors box. They are not exported as metrics (#145).
  • Device facts for the Overview: "Refresh identity" and the background network read now store the subnet mask and DNS servers (Wi-Fi and Ethernet), the relay names (the device's FriendlyName array) and the OTA URL the device would update from, read with one read-only Status 1 (never Status 0, never the OtaUrl command; credentials in the URL are removed before it is stored). Minimal firmware keeps the stored values. Migration 0031 (#145; OTA URL #127, thanks @Noschvie).
Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • Device page, Overview reworked (#145): a row of live tiles (Power and Energy today for devices with an energy meter, Wi-Fi signal with a quality bar or Link for Ethernet, Uptime, Reachability, Policy), a relay box that is the same for one relay and four (per-channel watts when the device meters each channel), Identity next to Connection, and one Activity list that merges state changes and transactions, newest first. An offline device gets an amber banner ("Offline since ..."); everything it last reported is dimmed and what needs the device is disabled, while Edit, Delete and Activity stay. The "Chip" row and the power sparkline are gone, the header Power button too (switch in the relay box). "All activity" opens the complete, paged list.
  • The device list, the dashboard and the device page now refresh themselves after the live stream opens or reopens when events were missed in between; the stream's address carries the page's event number (#160).
  • breaking: the UI listens on container port 8080 in every mode; the zero-configuration self-signed TLS no longer moves it to :8443. TLS or plain HTTP follows from the mode (nothing set: self-signed TLS; trusted proxies or ALLOW_INSECURE_HTTP: plain HTTP; certificate files: TLS), the port mapping never changes. TASMOFLEET_LISTEN and TASMOFLEET_FIRMWARE_LISTEN stay as bootstrap variables for non-container installs and saved containers, but the Unraid templates no longer ship them (#152).
  • breaking: TASMOFLEET_TLS_SELF_SIGNED is removed; a value that is still set is ignored with a start warning (it names the one case whose scheme changes, see Upgrade notes) (#152).
  • The Unraid templates keep ports only in the port mappings (Web UI host port 8443 to container port 8080, link https://[IP]:[PORT:8080]/), and the docker run and Compose examples use 8443:8080 and TZ (#152, #153).
  • The /metrics bearer token is now a setting: System settings, Metrics, "Scrape token" (write-only, stored encrypted, applies to the next scrape). TASMOFLEET_METRICS_TOKEN (and _FILE) stays valid and now locks the field: it wins over the page and is never copied into the database, so installs that set it, such as the Unraid template, keep working unchanged. An empty variable counts as unset (#151).
  • Device page: the Identity card shows the OTA URL the device would update from, when it is known (#127, thanks @Noschvie); the relay box uses the names the device gives its relays (Tasmota's default names stay "Relay N"); the Connection card of an Ethernet device shows subnet mask and DNS.
  • Device page: a device without a relay gets a Sensors box with the readings of its MQTT telemetry (dimmed while the device is offline), "Last restart" and "Offline since" follow the browser's time zone (Today/Yesterday), and the users page's "Last sign-in" does too (#145, #137).
  • The "Devices" crumb on a device page returns to the list as you left it, with its filter, search and sort, per browser tab; the navigation's "Devices" link still opens the unfiltered list (#127, thanks @Noschvie).
Fixed
  • Resizing a column no longer snaps back for a moment when two list updates overlap: an older answer that lands after a newer one now still gets the width you just set (it was judged by the newest request's start, so the old layout stayed on screen until the next update) (#166).
  • The device page follows the device live (a relay switched on the device, going offline, a finished transaction) without a reload (#126, thanks @Noschvie).
  • A device that went offline between the page render and the opening of the live stream, or during a stream outage, no longer keeps showing its old state until another event arrives (#160).
  • An empty secret file (TASMOFLEET_*_FILE with no content or only a newline) counts as not set, like an empty variable: it no longer locks the field with an empty value or clashes with the variable (#148).
  • "Today" and "Yesterday" (dashboard transactions, transaction history, Add devices "First seen", users page sessions and activity), the transaction log's clock times and the policy page's "paused at" now follow the browser's time zone like every other timestamp; before, the server's zone decided and one page could show two zones. Today/Yesterday also turn over at the browser's midnight without a reload (#137, reported in #128, thanks @Noschvie).

0.16.2

Added
  • Device name: a name you set by hand is still never overwritten by "Refresh identity", but the Identity card now says what the device calls itself ("Name on the device: ...", on the device page for as long as it differs) with "Use device name" and "Keep". Keep is remembered for exactly that DeviceName; the notice returns only when the device reports another one. A name tasmofleet filled in itself (the device's own DeviceName, its hostname, topic or address) still follows the device. Migration 0030 (#125, thanks @Noschvie).
Fixed
  • "Refresh identity" over MQTT (devices HTTP cannot reach) now reads the DeviceName too, with the plain Status (never Status 0); before it left the name untouched (#125, thanks @Noschvie).
  • The Chip of an ESP32 with the package ESP32-U4WDH (single-core: ESP32-U4WDH-S, e.g. ESP32-U4WDH-S v3.0 on a solo1 build) showed "Unknown" and Guard refused every update; the hardware table now knows it. The chip always comes from StatusFWR.Hardware, never from the build name (#155, thanks @Noschvie).
  • The version label in the header sits on the same line as the brand name (#143, thanks @Noschvie).
  • Policy page, empty state: the template cards no longer overlap the intro text, keep their titles and "Use template" links inside their borders, and stay within the page width (#131, thanks @Noschvie).
  • Add devices, network scan: "Stop scan" works. While a scan runs, only the progress and the results are refreshed, no longer the scan bar with the button, so a click is not lost to the refresh. Rows found so far can be ticked and added while the scan continues; ticked rows stay ticked and the "Add selected" button no longer flickers (#144).

0.16.1

Fixed
  • An environment variable that is set but empty (TASMOFLEET_LOG_LEVEL=, as the Unraid Community Applications template passes unfilled fields) now counts as not set: the default applies, the sign-in field is not locked and nothing is imported as a legacy setting. Before, an empty LOG_LEVEL stopped the container in a restart loop. The same holds for blank values and empty *_FILE paths (#148).

0.16.0

Added
  • The running version is shown in the page header, small and muted next to the brand (in the navigation drawer on a phone), and links to What's new. Builds without a release tag name the release they are based on: the CI images read 0.15.2+edge (<commit>) and 0.15.2+pr (<commit>), a plain docker build 0.15.2+dev. The same text is on the About page and in the startup log line (#124, thanks @Noschvie).
Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • Log: successful /healthz requests (the container health check) are logged at debug instead of info, log timestamps have millisecond precision (2006-01-02T15:04:05.000Z07:00), and the access log's duration is now duration_ms, a float in milliseconds (#132, thanks @Noschvie).
  • Timestamps in the web UI (audit log, state changes, device history, backups, restore jobs, bulk runs, console, "Last restart") are shown in the browser's own locale and time zone. The server renders <time datetime> with a fallback text in its own zone; time.js replaces it. Relative texts ("5 min ago", "Today 09:41") are unchanged (#128, thanks @Noschvie).
Fixed
  • "State changes" showed the stored UTC time while "Last restart" showed the server's zone; both now agree (#126, thanks @Noschvie). The wish for an automatic refresh of the list is not part of this change.

0.15.2

Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • The first log line at startup is now tasmofleet starting version=... go=... decoder=..., so the running version can be read from the log (#73).
  • The Devices list puts the "Online" filter pill right after "All", before Offline / unknown, Outdated and Not compliant.
Fixed
  • "Refresh identity" now always makes the stored MQTT topic follow the topic the device reports, also for devices added before 0.15.0 (a topic changed on the device used to stay unmatched, MQTT "Not connected"). A topic another device already has is not taken; the device page and the audit log say so. The change is audited as device.topic_changed (#111).
  • After "Refresh identity" the Restart confirmation dialog and the offline banner on the device page are swapped together with the header, so the dialog asks about the new name without a reload (#110, #104, thanks @Noschvie).
  • Devices list on a narrow screen: choosing a filter from the scrolling pill row (for example the Chip dropdown at its far end) no longer sends the row back to its start, so the pills stay where they were (#116).
  • After a topic change picked up by "Refresh identity", a device already heard on its old topic showed MQTT "Not connected" until its next telemetry: it kept the Offline that Tasmota sends on the old topic. Health now drops what it learned on the old topic and takes the new topic's retained LWT at once (#119).

0.15.1

Fixed
  • Device page, Network card: a device added (or deleted and added again, or given its MQTT topic) while TasmoFleet runs no longer shows MQTT "Not connected" or "other broker" until it next speaks. TasmoFleet keeps the latest LWT per topic and applies it as soon as a device takes that topic (#105, thanks @Noschvie).
  • The Devices list shows the "Online" filter pill at all times, next to Offline / unknown, Outdated and Not compliant; it used to exist only while the Online filter was active, so it could not be picked again after switching to All (#102, thanks @Noschvie).
  • After "Refresh identity" the device page header, breadcrumb and page title show the refreshed name, address, chip and firmware at once, not only the Identity card (#104, thanks @Noschvie).
  • A device stored under Tasmota's default name ("Tasmota", filled in when it was added) takes the name the device reports at the next "Refresh identity", so the header and the list no longer keep "Tasmota" (#104, thanks @Noschvie).

0.15.0

Fixed
  • "Refresh identity" now updates every identity field: device name, hardware, build, chip, firmware, hostname, MAC and MQTT topic. The hostname and IP are stored with the refresh (they showed Unknown until a later background read), and name and topic follow the device while they are still what the device reported before (or, for older devices, the hostname tasmofleet named them after). A name or topic you chose is kept. A device on our broker that HTTP cannot reach is refreshed over MQTT (Status 5, then Status 2, one at a time, audited); MQTT never replaces an identity read over HTTP (#91, thanks @Noschvie).
Security
  • "Refresh identity" checks the device's MAC first and refuses, audited and explained on the device page, when the stored address answers with another device's MAC: nothing is overwritten and nothing more is sent to it. A device stored without a MAC gets the reported MAC bound when a person refreshes it (audited); automatic refreshes never bind one (#65).

0.14.1

Fixed
  • Last seen: an offline device no longer shows a recent "last seen" time (for example "2m ago" in Needs attention). The broker's retained or last-will Offline message was counted as contact from the device; it no longer is, so the time is that of the last real message or poll. After a restart the device list, the dashboard and the device page all fall back to the same stored time of the last successful identity read (#92, thanks @Noschvie).
  • Offline devices and actions: the bulk bar of the device list now says how many selected devices are not online and will be skipped, and switches every action off when none of them is online; Back up now and Restart in a row's menu, and Restart and Update in the device header, are off for a device that does not answer, as are Settings and Console (with the reason as their title). "Online" is the health tracker's answer over every transport, so a device that is reachable over MQTT only stays usable. A device that is stale (silent for a moment, still being polled) now takes bulk actions like it already took the power switch (#93, thanks @Noschvie).

0.14.0

Added
  • Identity over MQTT: a device on our broker that HTTP cannot identify (unreachable, refused, not yet probed) now shows its firmware version, taken from the INFO1 message it publishes at every boot, read with the same version grammar as Status 2. When it still lacks its hardware, tasmofleet asks Status 2 once over MQTT (never Status 0; only online devices, once per device per 10 minutes, at most one per second on the bus; audited) and gets the full identity. The HTTP probe stays the primary source and always replaces what MQTT said; update safety checks are unchanged (#87, thanks @Noschvie).
Fixed
  • Identity: image tags are now read the way upstream Tasmota builds report them (#85). Official ESP32-S3, C2, C3, C5, C6 and C61 devices (all report tasmota32) and ESP32 display/ir builds were refused as a chip-family mismatch and missing from the fleet; they are now identified and keep their build. ESP32 webcam, bluetooth, lvgl, nspanel, zbbrdgpro and ESP8266 tasmota-4M are recognised instead of unknown. ESP32-S2 and P4 (two builds each with the same tag) stay unknown; after an explicit variant choice the P4 build follows the chip revision (p4 for v0.1-1.99, p4r3 for v3.1-3.99, others refused) and ESP32-S2 updates are refused until the build can be chosen, instead of guessing (#85, #45, thanks @Noschvie).
  • Identity: a firmware version with the core suffix that single-core ESP32 builds append (16.0.0(solo1)single-core, also the official 16.0.0(release-solo1)single-core) no longer aborts the identity probe and hides the device. The official solo1 build is recognised as tasmota on single-core ESP32; a self-compiled one keeps variant unknown, so OTA stays refused until a variant is chosen. A version that still does not parse no longer aborts the probe either: the device keeps its chip and hardware, the version stays empty and the safety guard refuses every update (#45, thanks @Noschvie).
Security
  • Container image: build, decoder and runtime stages move from Debian 12 to Debian 13 (gcr.io/distroless/cc-debian13:nonroot); clears 15 fixable OS findings; the image grows by about 4 MB (#77, #78, thanks @Noschvie).

0.13.1

Fixed
  • Discovery: devices seen on MQTT with an address outside the device networks (another LAN, e.g. behind a VPN) now become proposals as soon as that network is added to Device networks; before, they stayed under "Seen on MQTT, not identified yet" until a restart. A device later announced inside the networks no longer keeps its old reason in that list (#45, thanks @Noschvie).
  • Tests: an audit page test with fixed event dates started failing once they fell out of the default 7-day window; it now uses a fixed test clock (#74, thanks @Noschvie).
Security
  • Dependencies: golang.org/x/net v0.59.0 -> v0.60.0 (indirect). Fixes five HTTP/2 findings in the Go binary (one HIGH, four MEDIUM: CVE-2026-78669, -78659, -78660, -78663, -97032) (#76, thanks @Noschvie).

0.13.0

Added
  • Discovery: with a broker, MQTT is the primary discovery source. Devices that publish no tasmota/discovery config (SetOption19 1, or a build without USE_TASMOTA_DISCOVERY) are found from their own topics (LWT, STATE, INFO1..3, using the configured FullTopic): an online topic that is neither in the inventory nor announced gets one Status 5 over MQTT for its MAC (never Status 0; once per topic per 10 minutes, at most one per second, never to offline topics, logged and audited as discovery.identify). Switching MQTT discovery off stops it. HTTP scans stay (#47, thanks @Noschvie).
  • Discovery: "Seen on MQTT, not identified yet" on the Discovered tab lists the topics that could not become a proposal (offline, no answer, address outside the device networks) with the reason (#47, thanks @Noschvie).
  • Discovery: each discovered device has a direct "Add" that stores it as seen (MAC, topic, address) without an HTTP probe, also while it is offline; rows show "Offline" from the MQTT LWT. A device added this way gets its first identity probe when it comes online, after its address answered with its MAC (#45, thanks @Noschvie).
Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • README: the Docker Compose example sets container_name: tasmofleet (#62, thanks @Noschvie).
Fixed
  • Devices: requests to a device carry a Referer naming its address, so devices with Tasmota's default SetOption128 0 accept commands and firmware uploads instead of silently closing the connection (#63, #45, thanks @Noschvie).
  • Devices: when a device still refuses HTTP API commands (it answers its main page but not /cm, e.g. when added by a DNS name other than its hostname), the add flow and the logs say to set SetOption128 1 instead of "No device answered" (#63, thanks @Noschvie).
  • Discovery: a network scan of a device that MQTT had already proposed overwrote the proposal's topic, so the accepted device had no MQTT topic. Both sources now resolve to one proposal by MAC: MQTT keeps topic and source, the scan contributes the HTTP port (#47, thanks @Noschvie).
  • Discovery: an announced address outside the device networks is logged and listed instead of being dropped silently (#47, thanks @Noschvie).
  • Tests: the embedded test broker no longer deadlocks when clients disconnect while it closes (mochi v2.7.9 locks its client map recursively in Server.Close); this hung the first v0.13.0 tag run. It uses its own listener, which ends and waits for every accepted connection before mochi's shutdown, so mochi's client counter is no longer raced (race detector). A seed-timing flake of the pinned-columns e2e check is quarantined (#69).
Security
  • Discovery: the identity probe that a device without identity gets when it comes online (an MQTT trigger) no longer sends a stored web password to an address that cannot prove the device's MAC; it is audited as discovery.identify_unverified and retried every 10 minutes. The probe of a device without a stored MAC is audited with the MAC that answered (discovery.reprobe) and binds none (#47, thanks @Noschvie).
  • Discovery: accepting a device without MAC proof is audited with mac_unverified; for an offline device the fleet default password is withheld (default_password_withheld). A STATUS5 reply counts only for the topic discovery asked and only within 30 s (#47, thanks @Noschvie).

0.12.3

Fixed
  • Backup: the downloaded backup file is named after the device instead of a generic name (#55, thanks @Noschvie).
  • Backup: a Status 9 "Command Error" from a device without energy monitoring no longer marks a snapshot incomplete (#55, thanks @Noschvie).
  • Devices: a device's name defaults to Tasmota's DeviceName (labelled "Device name") instead of the hostname, in a scan, Discovery and the add flow. Names already stored are kept (#52, thanks @Noschvie).
  • Devices: the MQTT topic is the resolved one (tasmota_2BE970), not the setting tasmota_%06X. The unresolved topic never matched a message, so such a device showed "MQTT: Not connected" while it was online. A refresh repairs stored topics that still hold a placeholder (#52, thanks @Noschvie).
  • Devices: the upgrade resolves a stored tasmota_%06X topic from the device's MAC (Tasmota uses the last 6 hex digits of the Wi-Fi MAC), so existing devices show their MQTT state without a manual refresh. Other placeholders are repaired by the next identity refresh. A topic or DeviceName read from a device is validated (no wildcards, /, %, control characters) before it is stored (#52, thanks @Noschvie).
  • Devices: self-compiled builds that report 16.0.0(tasmota32) show "tasmota32 (self-compiled)" as Build instead of "unknown". The variant stays unknown, so OTA still needs an explicit variant choice (#52, thanks @Noschvie).
  • Devices: Ethernet boards show their Ethernet IP, gateway and MAC; the Wi-Fi card becomes an Ethernet card (#52, thanks @Noschvie).
  • Devices: the Network card no longer repeats the address as "IP address" (#52, thanks @Noschvie).

0.12.2

Fixed
  • Start: a data directory the container user (UID 65532) cannot write to, typically a bind mount that Docker created as root, now stops with data dir /data is not writable by uid 65532: see README (bind mounts) instead of an opaque secretbox error. The README explains chown -R 65532:65532 / user: for bind mounts and shows TZ in the Compose example (#33, thanks @Noschvie).
  • Settings: the MQTT connection test no longer reloads the page and blanks the password field. A password typed there passed the test, but the Save that followed saw an empty field and kept the old password, so the bus could not connect (#44). The test button now updates only its result line.
  • Devices: a device accepted from Discovery is probed right away, and every stored probe result (identity refresh, background network read) tells open pages to reload the table. Firmware, module and IP of a new device now show up on the open fleet page without a manual refresh (#46).
Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • Devices: the tooltip of "HTTP –" in the MQTT · HTTP column says the channel is on standby while MQTT delivers ("only polled when MQTT goes quiet"), so the dash no longer reads as "HTTP is down"; screen readers hear "standby". The column itself looks the same.

0.12.1

Fixed
  • Policy page: a long value (an OtaUrl) no longer squeezes the device name to one letter per line or pushes the value out of the card, in the live preview and in the "Make compliant" dialog. The name keeps a readable width and the value wraps below it, breaking inside the URL.
  • Policy page: two chip-family-limited rules for the same setting (e.g. OtaUrl for ESP8266 and for ESP32) no longer claim to overlap on devices of unknown chip family, which neither rule can judge.
  • CI: TestDevenvDesignFixture no longer flakes ("Kitchen Ceiling Light (unknown) answers on 192.0.2.1:…"): devenv puts devices that never answer on a privileged loopback port (192.0.2.1:1, :2, …) instead of a released ephemeral port that a simulator started right after it could get again (#39).

0.12.0

Added
  • Fleet policy: OtaUrl is a supported setting (policy editor, group "Firmware"). A rule can require a device's firmware URL, e.g. http://ota.tasmota.com/tasmota/release/tasmota.bin.gz for esp8266 and http://ota.tasmota.com/tasmota32/release/tasmota32.bin for esp32, so devices imported from TasmoAdmin stop pointing at a dead host; "Make compliant" sends OtaUrl <value>. The value is read live and from backups (ota_url); credentials in a URL are never shown. The setting is policy-only: the per-device settings form neither reads nor writes it.
Fixed
  • The job log reads as the update went: a command that timed out while the device was flashing (ESP32 with the safeboot layout) shows "Device busy, still flashing..." instead of "Command failed", a cancel names the person instead of the 64-character id, and the verify step says "Updated to <version>" (or "Not on the new version yet") instead of "Identity"; a cancel that came too late is said so. The quiet lines of an update name the firmware they wait for ("Rebooting into the minimal firmware...", "Waiting for 16.0.0 to answer..."), and a staged update says when the minimal image is installed and the final one follows.

0.11.6

Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • Push where possible: a device whose free flash (Status 4 Free, read with the preview's identity) is too small for the image, or unknown, gets the download instead, and the preview says so ("not enough free flash for an upload (image 2183 KB, 697 KB free)"). This covers ESP32s with the safeboot partition layout, which can only take a full image through the download.
Fixed
  • Push upload: when the device refuses the upload ("Upload Failed", e.g. "Not enough space" on an ESP32 with the safeboot layout) or offers no upload form, the job now fails at once ("upload refused, nothing was flashed: ...") instead of waiting for the end of the upgrade window. Nothing was flashed and the device kept running.

0.11.5

Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • The Delivery choice of an update is "Download (all devices)" (the default, as before) or "Push where possible (ESP32; others download)". Push no longer refuses devices that cannot take an upload: an ESP8266, an unknown chip, a device on safeboot or minimal, or a two-step plan gets the normal download instead. The preview says per device which delivery it gets and why, the job stores the delivery it really uses, and the transaction page marks the devices updated by upload. The strict push mode (refuse such devices) is gone.
Fixed
  • A release tag now publishes its image: the publish job in ci.yml was skipped because the implicit success check counted the skipped jobs before required (v0.11.4 shipped no image from its first tag run).
  • Push upload: a stalled upload no longer leads to a second upload. The first real ESP32 push hung for the whole upgrade window, after which the job timed out without probing once and sent the image again. The window for the device to drop off or reboot now starts when the upload has ended, the upload's own error is kept in the job history (with how many bytes went out), and a push job sends its image once: at the end of the window it fails instead of starting the stage again. The upgrade page (/up) has its own short timeout.
  • A cancel requested while a device was in its upgrade stage could be followed by a new attempt (OtaUrl and Upgrade 1, or another upload) when the stage timed out; the job now ends cancelled there.

0.11.4

Added
  • Static demo on GitHub Pages (<https://nofuturekid.github.io/tasmofleet/>): make demo renders the design fixture fleet (60 simulated devices) with the real binaries and saves every page an admin reaches by plain links as static HTML in dist/demo (forms, htmx requests and the live stream are disabled; addresses are documentation ranges). The pages workflow builds and publishes it on release tags and by hand. make demo-media renews the README's screenshots and update animation in docs/media.
Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • The network facts (hostname, IP, gateway, SSID, BSSID, channel from Status 5 and Status 11) are read in the background and never hold anything up: first contact after an import or add and "Refresh identity" no longer wait for them (Status 11 is not sent there at all; Status 5 is still read for the MAC). The server reads them, one device and one command at a time, once a device without facts is online and after each identity refresh, and the Network card fills in when they arrive.
  • CI runs in tiers. Pull requests run the FAST tier (lint, unit and integration tests from the go test cache, the @core e2e tests in two shards, and the race tests and mutation gate when a safety-critical package changed); the nightly run, release tags and manual runs run the FULL tier (every e2e test, the race detector, uncached tests, the full mutation run). A release tag publishes its image only after FULL passed. Known-flaky tests are quarantined (one flaky issue each) and run only in the nightly job, with retries. make check now includes the integration tests; new targets make e2e-core, make safety, make test-quarantine and make e2e-quarantine.
Fixed
  • The device list kept the browser busy while device states streamed in: every live cell update (status, uptime, power, ... of each device) re-ran the table layout script, which since the pinned-column fitting below measures the content of every row. With the 60-device design fleet the list needed over two minutes to show every device online (5 s before, 5 s again now); on a slow machine clicks waited tens of seconds. Cell updates now refit the table at most every 250 ms, and a cell whose content did not change is not measured again.
  • The top bar's user menu was drawn below the device list's Columns button, which showed through it. The z-index layers are now one documented scale (sticky parts, menus, top bar, top-bar menus, dialogs, toasts) in app.css, and an e2e check asks the browser what is on top at every point of each open top-bar menu.
  • Device list at the default layout: a device with two relays had two Power switches in a column narrower than both, so the second was cut off and an ellipsis showed between them. Pinned columns now always get the width of their content, also without a saved layout (the 0.10.1 guarantee), and the offsets are measured again once the fonts are loaded. The columns pinned to the right no longer cut a scrolling column in half at the start of the scroll range (Uptime read "seen 1"): the wide columns before it give up the missing width, so it ends where the pinned columns begin. An e2e check keeps pinned columns from clipping or covering content.
  • README quick start: the first sign-in used docker exec tasmofleet cat /data/initial-admin-password, which fails (the image has no shell tools); it now uses the setup link from the container log.

0.11.3

Added
  • Device page: State changes and Transactions show the newest 10 (was 20), each with a link to the complete list: a new per-device history page with paging (/devices/{id}/history), and /transactions?device=ID, which lists every transaction that device took part in.
  • Retention for transactions: finished transactions and their logs older than the audit retention (AUDIT_RETENTION, default 365 days) are deleted daily. A running transaction, or a transaction with one unfinished job, is never touched; a transaction that a kept retry points at stays too.
Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • A device's first observation after the app started is a baseline, not a state change: a restart no longer logs "Unknown to Online" for every device. The cost: a change that happened while the app was down is not recorded.
  • Power & energy card: a device without readings says "No readings yet", and one that drew 0 W the whole time says "No load in the last 2 h", instead of an empty or dashed chart.
Fixed
  • Device header: the Power On/Off control was an inverted (light in the dark theme, dark in the light theme) pill while the switch was on; it now keeps the look of Restart and Back up in every state and theme, and the switch alone shows On. A power switch outside the table and card cells no longer inverts either.
  • Device overview: the State changes and Transactions row touched the cards above it (a column box drops its last card's bottom margin). All gaps between cards use the new --card-gap token, and an e2e check fails when two sibling cards come closer than that.
  • Device overview: the Columns button of the State changes and Transactions tables sits in the card head, on the title's line, instead of a row of its own.
  • e2e: the devices toolbar check measures in one in-page step (the table part reloads 0.5 s after the first health reports and left a detached frame behind), and the phone audit budget covers the spread of the audit page's system rows (3370 to 3414 px on the same seed).
  • Device page: Hostname, Gateway, SSID, BSSID and Channel no longer depend on a backup snapshot. Every probe (first contact after an import or add, "Refresh identity") reads Status 5 and Status 11, one at a time and never Status 0, and stores the facts on the device (migration 0026). The newest snapshot stays the fallback for devices not probed since.
  • Network card: the row that showed the configured host as "IP address" is now "Address"; the device's real IP (Status 5) is its own "IP address" row.

0.11.2

Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • The project moved to GitHub (github.com/nofuturekid/tasmofleet) with a fresh history; container images are published to ghcr.io/nofuturekid/tasmofleet.
  • License: AGPL-3.0-or-later (was MIT). The About page links to the source code.
Fixed
  • Dark theme: the popups of native selects (the Chip and Tag filters and the Sort menu on the device list) showed light option text on the browser's white popup, unreadable. Every option now paints the theme's surface and text colours. A new e2e guard checks every page in light, dark, Auto and an explicit choice over the system setting: each form control resolves color-scheme to the theme in force, and every option has an opaque theme-coloured ground with readable text (screenshots never show native popups).
  • Device list: the "Not compliant" chip read "0+" while part of the fleet was not checked; it now shows the known count, with "n not checked yet" in its tooltip (as the dashboard tile says it). It still shows "–" while nothing is checked.
  • Device list: "All" kept a health filter from a dashboard tile (?health=online), so the list stayed narrowed. All (and every other chip) now drops the health filter: from any dashboard tile, All shows the whole fleet and the URL carries no status filter.
  • Behind the HTTPS proxy every live-update stream ended after 20 s (since 0.11.1), so /devices kept showing "Live updates interrupted". The stream's periodic session re-check looked for the plain-HTTP session cookie; it now sees the request's scheme and base path. The same re-check in the discovery and firmware-download streams is fixed too.
  • The connection banners wait: a stream that comes back within 15 s (and fewer than three failed reconnects) no longer shows "Live updates interrupted" or "Reconnecting" in the top bar. When a banner shows, it floats below the header instead of pushing the page down.

0.11.1

Fixed
  • Break-glass sign-in over plain HTTP on the container's own address (e.g. http://tasmofleet.example.internal:8080) failed with "missing or invalid CSRF token" when PUBLIC_URL was https://...: every cookie was Secure and __Host- prefixed, so the browser dropped them. The Secure flag and the __Host- name now follow the scheme of each request (own TLS listener, or X-Forwarded-Proto from a TRUSTED_PROXIES address), never PUBLIC_URL. The OIDC redirect still uses PUBLIC_URL.
Security
  • Behind a reverse proxy, cookies are Secure only when the proxy is listed in TRUSTED_PROXIES and sends X-Forwarded-Proto: https. Without TRUSTED_PROXIES the public HTTPS site now gets non-Secure cookies; set it for the proxy (the reference deployment already does). A plain-HTTP session cookie is sent in clear text on the LAN (accepted for break-glass in a homelab).

0.11.0

Added
  • Settings → Backups shows the backup storage (design v2 N9, package W5): number of backups, total size, kept backups and devices without a backup, the ten largest devices with a link to each device's backups, and the backups of deleted devices (kept with "keep the backups") with Download, Keep and Delete. Deleting such a backup now returns to Settings → Backups instead of a missing /devices/0/backups page.
  • "Reset settings…" on Settings → Advanced puts every setting stored in the UI back to its default after one confirmation and the step-up (POST /settings/system/reset, audited as "Settings reset"). Settings from the environment, the first-run setup state and a paused policy live check are kept; devices, backups and users are untouched.
  • Design v2 components (port package F4): internal/web/ui gets the design system's components with typed props (page header, buttons, badges, status dots, avatar, cards and key/value rows, form fields with the server's error, radio cards, segmented controls, switches, stat tiles, the fleet health bar, the attention list, transaction card and rows, tabs, row menus with disabled reasons, confirmation and step-up dialogs, confirmation pages, banners, toasts, empty / no match / loading states, the error page, the sign-in card, the console, the backup comparison and release notes). Pages adopt them as they are ported.
  • make styleguide-docs writes a screenshot of every /styleguide section (desktop and phone, light and dark) into docs/design/styleguide/; docs/spec/design-system.md is rewritten for v2 and embeds them. A test fails when a section has no doc entry or no screenshot.
  • Buttons with data-toast show that toast (toast.js).
  • Backups can be kept (design v2, N1): POST <base>/backups/{id}/keep with kept=1|0 (operators and admins) keeps or releases a whole backup run. Kept backups are never pruned and do not count against BACKUP_KEEP. Each backup records what started it (manual, scheduled, before an update with the target release, restore, replace, bulk). A failed backup is recorded as an attempt with a reason that never contains the device URL or credentials; attempts are pruned with the audit retention. "Delete selected": POST <base>/devices/{id}/backups/delete (admin, id repeated, confirm=1) deletes whole runs of that device only. A bulk backup takes keep=1 ("Keep these backups") and label (stored with each dump). The pages show these with the backups page port.
  • Delete a device but keep its backups (design v2, N2): the device delete takes backups=keep|delete and the acknowledgement ack=1 (checked on the server). Kept backups stay with the device's name and MAC and are listed as backups of a deleted device (Settings, with N9).
  • Users v2 backend (design v2, N4; the pages follow with the users page package): local accounts get a display name, an e-mail address and "created at / by" (migration 0023). Admins can create an account or reset a password with a generated one-time password (form field otp on POST <base>/users, action otp on POST <base>/users/local/{name}): it is shown once, stored hashed, never logged or audited, must be changed at the sign-in and is refused after 24 hours. GET <base>/users/{name} and GET <base>/users/principal/{subject} show a user's details, live sessions and last 10 audit entries; POST <base>/users/sessions with a session field ends one single session; POST <base>/account/profile sets the own profile; the action profile sets another account's. SSO and proxy users keep the e-mail of their last sign-in.
  • Sign-in extras (N11): the sign-in page data carries the attempts left before the rate limiter delays sign-in (a count only), a "session expired" flag (the login redirect gets expired=1 when the request's session ran out) and the "Keep me signed in on this device" choice (form field remember).
  • Setup step 1 (N12): at first start the log line that reports the created admin account also carries a one-time setup link <base>/setup/account?token=.... It sets the admin's password, signs the admin in and continues with the setup. The token is valid only while the admin still has its first-start password (every start prints a fresh one until then) and is never stored, shown in the UI or audited.
  • User detail, one-time password and setup step 1 pages (design v2, N4 + N12 UI, package W2): a user's page (/users/{name}, /users/principal/{subject}) shows who, role and state, the actions as buttons (each confirmed in a dialog and answered on the same page; a forbidden one stays disabled with the reason; while a step-up is due they open "Confirm it's you"), the deactivated banner with Reactivate, the SSO / proxy note, Details (with "Edit name and email" and "Set a password instead" for local accounts), the live sessions with End session (never on your own) and the last 10 audit entries with a link to the audit log. A generated password has its own page with Copy, the expiry and Done; it is in the page exactly once and never cached. The setup link's page shows step 1 of 3 ("Open the setup link": where the link is, no form) and, with the token, the first admin's account form (step 2); the wizard is step 3. Copy buttons can read their value from an element (data-copy-from, copy.js).
  • System settings (design v2, N17-N20; the v2 settings page renders them, P8):
    • Log level (LOG_LEVEL: debug, info, warn, error) is a settings-page field in the Logging group (General tab) and applies live through one shared slog.LevelVar; every change is audited. Set in the environment, it locks the field.
    • MQTT broker as Host / Port / TLS: config.SplitMQTTURL / config.JoinMQTTURL map MQTT_URL to the three parts and back (round-trip tested). The settings form and the connection test accept mqtt.host, mqtt.port, mqtt.tls and compose MQTT_URL (tcp:// or ssl://); MQTT_URL stays the stored value.
    • `/metrics` master switch (METRICS_ENABLED, default on): off, /metrics answers 404; applies live.
    • Fleet default device password (DEVICE_DEFAULT_PASSWORD, write-only secret, sealed like device passwords, never shown or audited in clear): adding a device and accepting a discovery proposal take it when the form sends use_default_password=1 and no password was typed; the web user stays Tasmota's admin.
  • Backend for the design v2 counts and options (core group g5; the pages render them later):
    • N7: one-pass fleet counts for the dashboard tiles and the device chips (Unknown apart from Offline, devices behind the newest cached release, devices and rules with policy deviations, discovery proposals found but not adopted and address changes for admins) and the device list filter ?chip=down|outdated|noncompliant.
    • N8: the health tracker keeps a two-hour, one-sample-per-minute power history per device from the ENERGY telemetry it already receives (in memory only).
    • N6: the firmware cache can list the cached feed without contacting a source (CachedReleases), and "changes since" helpers list every release between the oldest version in the fleet and a selected one, with breaking sections flagged.
    • N14: the audit log filters by action family (action=device.*), a search box (q=, also device and SSO user names) and since=today, in the store.
    • N15: bulk commands and restarts take wait=1 (wait up to 5 s for each response), serial=1 (one device at a time) and parallel=1|3|5 (devices at a time). A capped transaction runs against its own cap instead of the fleet-wide limit; migration 0024 stores the options on the jobs so a resume and a retry keep them.
  • What's new, backend (design v2, N5; the page and the avatar dot follow with the shell's user menu): the binary embeds CHANGELOG.md, internal/changelog parses it into releases, sections and items (code, bold and safe links only, everything else stays text), and the user preference news.seen keeps the newest release a user has seen. Only released versions of the running binary are accepted for it; a user who never opened What's new counts one new release.
  • About lists more components (design v2, N13): the release feed (last check, stale or failing without the fetch error, which can contain a source URL), the database (SQLite version and size) and background jobs (running and queued), after the MQTT broker and the backup decoder.
  • Theme choice per user (design v2, D11): Light, Dark or Auto (follow the system, the default), stored as the user preference theme (POST <base>/prefs/theme) and rendered by the server, so pages open in the chosen theme without a flash.
  • VISUAL_REGION=shell make visual: compares only the header and the crumbs of every reference at the foundation tolerance (summary and e2e/test-results/visual-shell/report.html); the logo references are compared as 6x crops of the top-bar brand.
  • make visual-parity BASE=<commit>: renders every page without clicks with binaries built from BASE and with the current ones and compares the screenshots with tolerance 0, for packages that must not change how pages look.
  • make visual: pixel comparison of the app's pages against the 1312 reference screenshots (prototype render v6) of the second Claude Design prototype (design v2 port, work package V0). It renders each page with the design fixture, masks the values the app cannot reproduce, compares with pixelmatch and writes e2e/test-results/visual/report.html. The prototype, the SHA-256 manifest of the references and the fixture generator are in design/v2/ and design/tools/v2-fixture.mjs. Not part of CI; the screens of later packages fail until they land.
  • tasmofleet-devenv -design-fixture: starts the prototype's 60 devices (names, chips, firmware, power state, TelePeriod, PowerOnState) and its release list; POST /fixture/offline on the control server takes the devices the prototype shows as offline away after the app has seen them. simulator.DeviceSpec gets Power and Settings (initial relay and flash state), and a device can be PowerOffed (will published, HTTP port closed).
  • Add devices, design v2 (port package P6): "Test connection" on the Add manually tab probes the address with the typed password and says what answered, without adding the device; "Add…" on a discovered device opens the form filled from it (/devices/new?proposal=<id>, a scan result ?address=&port=). The Discovered tab selects several new devices: POST <base>/discovery/proposals/accept and .../dismiss with id=<id>:<version> repeated (admins; each accept is still tied to the version shown; an id without its version is refused as out of date and nothing is added). The import result has "Download report" (GET <base>/import/tasmoadmin/report.csv, the last result of the session, for 10 minutes).
  • Add a local user on its own page (design v2, P9): GET <base>/users/new (admins) with Name, Username, Email, Role, and the choice between a generated one-time password and a typed initial password (typed twice; a mismatch is refused). A refused form comes back with what was typed.
  • Delete a device on its own page (design v2, N2, package W4): GET <base>/devices/{id}/delete lists the device's backups (count, total size, newest, kept ones) and asks whether to keep them (preselected) or delete them too, with one acknowledgement; it posts backups and ack. The device header's Delete and the list's Delete… lead to it; a refused post shows the page again with the field error. Works without JavaScript.
  • The device overview draws the power of the last 2 hours as a bar sparkline next to "Watts now" (design v2, N8, package W4): one bar per 5 minutes, a gap where no reading came in.
  • What's new page (design v2, N5): GET <base>/whats-new (every signed-in user) shows the changelog of the running binary per release, with the installed release and the releases new since the last visit marked; opening it records the visit. The avatar has a dot while releases are unseen, the user menu says "N new since last visit" and the phone drawer lists What's new. About's "What's new in x.y.z" and its Changelog link open this page instead of GitHub.
  • Release page (design v2, N6): GET <base>/firmware/releases/{version} (operators) shows a release's whole notes with the breaking section in a box, the files this fleet needs with their cache state and Prefetch / Retry, and how many devices are on each older version; ?view=since lists every release between the oldest version in the fleet and this one, newest first, with breaking changes flagged. It reads only the release list already in memory (an empty state before the first check, never a request upstream); an unknown version is a 404. The firmware page's selected release links to it ("Release page", "Changes since <oldest>").
Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • The last html/template pages are gone (design v2, Z1): result and refusal notices ("Nothing to retry", "Command started", ...) and the transaction retry page are templ pages built from the design system's page header, card, banner and buttons; the device list's rows, phone-card cells and the cells the event stream pushes are templ components (cells.templ), so a live update renders exactly like the first paint. internal/web/templates/, the legacyContent bridge, the unused v1 device form, jump.js and the .jump rules are removed.
  • Design v2, the Updates pages (port package P4): /transactions shows the running transactions as cards with their progress and the history newest first (id TX-0143, what it did, devices, result, who and when); it is no longer a sortable table with a Columns chooser. A transaction's page has the state, the progress bar with done / in progress / queued / failed, one row per device with its versions, state and progress, and the log of one device beside it (?job=; a failed device opens first). "Cancel remaining" and "Retry failed (N)" are the header buttons ("Retry failed" waits until no device runs); cancel and retry of one device and "Copy log" sit in the log's head (without JavaScript, "Cancel remaining" opens a page with the dialog's words). A "Make compliant" transaction shows each device with its changes and their state. /updates/new is the prototype's "New firmware update": 1 · Devices (opened without a selection it ticks every outdated device that can be updated; a "Select" menu replaces the quick-selection buttons), 2 · Release as cards (uploads included, older releases folded), 3 · Options (all visible: "Back up each device before flashing", policy, power after update, delivery, variant) and a sticky Plan with the devices, firmware files and an estimated time. "Review update" opens step 4, the preview with "What happens", "Start update" and "Change" (back to the picker with the same choice).
  • breaking The transaction page refreshes through one route, GET <base>/transactions/{batch}/live (?job= keeps the open log); GET <base>/transactions/{batch}/summary, /table and /jobs/{job}/row are gone. The picker posts backup=1 with backup_shown=1 ("Back up each device"); skip_backup=1 is still the preview's field. POST <base>/updates/new also takes the release and the options (the preview's "Change"). Saved column layouts and sort orders of the transactions, update picker and update preview tables are ignored (the tables are gone).
  • Add devices, design v2 (port package P6): the screen has the prototype's four tabs, Add manually (/devices/new), Discovered (/discovery), Network scan (/discovery/scan, new route) and Import from TasmoAdmin; the old "Scan and discovered" page is split. Breaking for scripts that read the pages: the add form shows the address, the fixed web user admin and the web password; name, port, tags, notes and the OTA exclusion moved under "More options" (still saved). The fleet default password (N20) prefills only a form whose address was typed, never one filled from a discovered device. Discovered lists address changes (Dismiss / Apply) above the new devices; "Show ignored" lists ignored devices in the same table with "Restore". The scan tab shows the latest scan with All / New / Known / Ignored filters and a next step per row; the scan history list is gone. The import has one "Choose files" (or drop) for devices.csv, MyConfig.json and the key file (form field files, sorted by file name; the three named fields still work), uploads on choosing, and shows the plan as Create / Merge / Reject rows. The Add devices tables have fixed columns: no Columns chooser and no sort links any more. The "Add devices" badge counts address changes too.
  • Discovery proposals carry the first sighting (FirstSeen, the "First seen" column).
  • Design v2, system pages (port package P8). System → Settings is the prototype's screen: "Settings" with its ten tabs (General, Network, MQTT, Discovery, Devices, Backups, Firmware source, Metrics, Sign-in, Advanced; ?tab=) and one card with Discard and Save settings. The fields are the real settings in the prototype's components: log level as Error / Warn / Info / Debug, the broker as host, port and Use TLS (N18), on/off settings as checkboxes (the metrics switch as a switch), the sign-in method as radio cards, the default device password under Devices next to the fixed web user admin; the Discovery tab starts with the state of MQTT discovery (listening, off, waiting for or missing a broker) and "Open Discovered". Test connection and Test discovery show their result next to the button. A sign-in change that needs a confirmation opens "Confirm it's you" over the tab: the password for a local account, the identity provider for a single sign-on account, back on the same tab with what was typed. Audit log gets the prototype's header ("kept for N days" from the audit retention) and filter row: search, actor, action family, period and result, applied as soon as a select changes (a Filter button without JavaScript); the events stay a table with sorting, Columns chooser, inline details and the pager. The first-run setup is the prototype's last step, "Basic settings", on a page of its own: the steps indicator, device networks, broker host, port, user, password and TLS with Test connection, and "Scan these networks for Tasmota devices when I finish"; Finish setup saves, ends the setup and starts that scan.
  • breaking: GET <base>/settings/system takes ?tab= (general, network, mqtt, discovery, devices, backups, fwsource, metrics, signin, advanced) instead of ?section=; on/off settings are posted as a checkbox v.KEY=true with the marker b.KEY=1 (without v.KEY the setting is saved as off). POST <base>/setup/save is gone: POST <base>/setup/finish takes the setup's settings (v.DEVICE_NETWORKS, mqtt.host, mqtt.port, mqtt.tls, v.MQTT_USER, v.MQTT_PASSWORD) and scan=1.
  • Design v2, the style guide (port package F4): /styleguide is the prototype's UI-states screen (inline banners, toasts, the live connection indicator) followed by the gallery of every component, and it is open to every signed-in user (it shows sample data only), no longer to admins only.
  • The navigation is the same for every role: operators see the whole menu (System included) and each page enforces its own permission; a group button leads to the first page the role may open (an operator's System button opens the style guide).
  • The confirmation pages without JavaScript (delete or restart a device, delete a backup or a policy rule, cancel a transaction or an update) use the design v2 confirmation look.
  • Row menus built with ui.RowMenu close on Escape and on a click elsewhere, one open at a time; opening a dialog closes the user menu as well as row menus.
  • Database migrations 0021 and 0022 (design v2, N1/N2): backups gains kept, trigger, trigger_ref, device_name and device_mac, and backups.device_id becomes nullable with ON DELETE SET NULL (the table is rebuilt; the rows keep their ids). New table backup_attempts. Deleting a device no longer removes its backup rows by cascade: the caller decides. A device delete without a choice (the v0.10 dialogs) still deletes its backups, and now also their files, which used to stay on disk.
  • breaking Policy rule order is precedence (design v2, N3): when two enabled rules check the same setting on the same device, only the higher one is evaluated there, so a device is no longer flagged for two contradicting values. Rules can be disabled ("disabled": true in the POLICY_RULES format): a disabled rule is kept and versioned but flags nothing. The editor preview counts each rule with precedence and returns the overlaps (conflicts with the number of shared devices). New form routes (admin, CSRF, version-checked like save): POST <base>/policy/rules/order (order=2,0,1 or idx + move=up|down), /policy/rules/duplicate (a disabled copy below), /policy/rules/enable, /policy/rules/disable and /policy/rules/template (the four starter templates as Advisory rules). The editor form accepts state=enabled|disabled; without it a saved rule keeps its state. The policy page UI for these comes with the page port.
  • breaking Design v2, the policy page (port package W1, N3 + N21): <base>/policy is the prototype's page and holds the rules editor. Rules are cards in their stored order (the order is the precedence) with a drag handle, Disabled badge, Required / Advisory, the devices they flag now and a menu (Duplicate, Enable / Disable, Move up / Move down, History, Delete…; selecting a card opens its editor, so the menu has no Edit); the editor has the Enabled switch, "Applies to" (chip families, tags, devices), Setting and Expected value, Exceptions, Note, "When a device deviates" (Advisory / Required) and the change note; the live preview names each device the open rule flags with its value and where the value came from; the version history compares a version with the current rules and reverts after a confirmation. A banner names every conflict ("… wins for N devices they share"). Without rules the page offers the four templates. The live-check status line shows the interval and the last run, "Paused by … at …" with Resume (N21), or a failure with Retry now, and keeps Run now. "Make compliant (N)…" opens a dialog with the exact commands of every device with a Required deviation read live; without JavaScript it posts to the plan page. Advisory rules are no longer part of Make compliant. The device table of the old page (status chips, Source column, per-device Make compliant) is gone: a rule's flagged devices are in its live preview. Import and export moved to <base>/policy/io (Export tab with Copy and Download .json, Import tab with a preview of the changes before anything is saved).
  • A local sign-in without "Keep me signed in on this device" now sets a browser-session cookie (no Max-Age): the session ends when the browser closes. The server-side idle and absolute limits are unchanged. Re-issued sessions after a password change are browser-session cookies too.
  • breaking: an unknown TASMOFLEET_LOG_LEVEL now stops the start (it used to fall back to info silently). A LOG_LEVEL in the environment now locks the settings-page field; the Unraid template and the env example leave it empty, so remove TASMOFLEET_LOG_LEVEL=info from an existing container to choose the level on the page.
  • Design v2, the shell (port package F3): a new top bar with the TasmoFleet mark and name, the four groups Dashboard, Devices, Updates and System as links, and below it the group's pages (Overview; All devices, Add devices with the number of discovered devices, Policy; Transactions, Firmware; Settings, Audit log, Users, About, Styleguide). The bar and the sub-navigation keep their content in the same 1640 px column as the page, their backgrounds span the window. "New transaction" left the menu (the Transactions page has the button). Every page has breadcrumbs. On phones a header with the page title opens a navigation drawer; without JavaScript the menu button leads to the new page /nav. The user menu (avatar) holds the name and role, a Light / Dark / Auto theme switch, "Account & password" for local accounts and Sign out. Pages with live updates show the connection in the top bar (Live, Reconnecting…, Offline) and a banner while the stream is down, with "Retry now" once it is offline. Confirmations after a redirect ("Saved.") are toasts bottom right that fade after 4.5 s (an inline banner without JavaScript). The About page is the first page built in templ: version card, components (MQTT, backup decoder) with their state, and the effective configuration as before; the four summary cards (sign-in mode, public URL, base path, poll interval) are gone, the values stay in the configuration table.
  • The product is written "TasmoFleet" in the UI, the page titles, the README header and the web app manifest; identifiers (repository, binary, image, TASMOFLEET_*) stay lower case. The logo is the design v2 mark, a white ring on a blue tile: favicon, app and home-screen icons, Home Assistant add-on icon and logo and the README wordmark are regenerated from assets/branding/logo.svg by design/tools/brand-icons.sh; logo-dark.svg is gone (the mark is the same in both themes). internal/web/static/SHA256SUMS now also lists the icons, and a test checks every listed checksum.
  • The event stream asks browsers to reconnect after 4 s (retry: field), the delay the "Reconnecting" banner shows.
  • Design v2, templ toolchain (port package F1): every page now renders through a templ page shell (internal/web/ui); pages that are not ported yet run inside it through a bridge that executes their html/template content block. No visual change: the page markup is the same as before and the pages render pixel-identical to the previous build at 1280 and 375 px in light and dark (make visual-parity), apart from antialiasing noise at the corners of one select. New runtime dependency github.com/a-h/templ; the generator is pinned in tools/go.mod, its output is committed, make templ regenerates and make templ-check (in make check and the CI lint job) fails on stale generated files. Inline script and style stay forbidden (lint rule, source test, render test over every page family).
  • Design v2, step 1 of the port (tokens and fonts): the colours, shadows, radii and fonts are now the Claude Design v2 prototype's, in light and dark. The accent turns from teal to blue, surfaces and controls get rounded corners, section rules become hairlines, and the text is set in Geist (Geist Mono for addresses and versions) instead of Archivo, self-hosted, versioned and preloaded like Archivo was. Layout and markup of the pages are unchanged; the shell, components and tables follow in the next steps.
  • The phone layout now applies below 720 px (45rem) instead of 768 px, as in the prototype.
  • Design v2, tables and bulk bar (port package F5). Tables show from 720 px (was 1152 px) with all their columns; a table wider than the page scrolls sideways inside its frame instead of hiding columns. Columns can be pinned to the left or the right edge ("Pin left" / "Pin right" in the Columns chooser, saved per user with the layout) and pinned columns stay in view while the table scrolls. In the device list Name is pinned left and Actions right for good (a saved order that moved a column in front of Name now starts with Name), Power is pinned right by default (it can now be unpinned and moved); in every other table the row actions stay at the right edge. A pinned column is never narrower than its content, and an open row menu is never cut off by the scrolling table. The Columns chooser is the prototype's: drag a row by its grip or use ↑ ↓, Pin left, Pin right, "Reset to default". On a phone the device list shows cards (name with status dot, address, chip and firmware, status, uptime and signal, power and row actions) with "Select all N"; ticking a card ticks its table row and the other way round. The bulk bar of the device list is the prototype's: a dark bar centred at the bottom on a wide screen, a panel with the actions in a grid of three on a phone, without the folded "Actions" menu. It offers Update firmware (new: opens New transaction with the selection; POST <base>/updates/new takes the selection as a form post), Send command, Back up, Restart, Power on and Power off (were "Switch on" / "Switch off") and Clear (was "Clear selection").
  • Design v2, device list (port package P2). The page is rendered with templ (devices.templ; templates/devices.html is gone) and follows the prototype: the header counts "N online · N offline · N unknown" with Scan network and Add devices; one toolbar with the search ("Filter by name, IP, chip or firmware"), the filter chips with their counts (All, Offline / unknown, Outdated, Not compliant), and Chip and Tag selects listing what the fleet has with a count each (they replace the Filters fold; the Health select is gone, a health= link still filters); on a phone a Sort select. The table's columns are the prototype's: Status (badge with dot and "N deviations"), the new MQTT · HTTP column (live, event reach-<id>), Firmware with an "update" badge, Chip (written ESP32-C3), Address, Uptime, Wi-Fi as a signal bar with dBm, Last restart (now shown by default), Power as a switch per relay (disabled while the device does not answer) and the "⋯" row menu: Open, Open console, Back up now, Restart…, Delete…. The menu's Settings entry is gone (the device's Settings tab), Delete… leads to the delete page instead of a dialog, and Back up now answers with a toast ("Backup of Attic Dimmer saved") instead of a line above the table. "Select all shown" is gone (the header box selects). Phone cards show "Update available", "N deviations" and "MQTT down". Empty fleet: "No devices yet" with Scan network, Add manually and Import from TasmoAdmin; no match: "No devices match these filters" with Clear filters. The Uptime of an offline device reads "seen 3h ago" (the Status column no longer shows the age). The device table keeps the prototype's pixel widths: it scrolls sideways when they need more room, and a saved layout keeps the table's size, so widening a column no longer squeezes the others. A row menu entry that acts in place (Back up now) closes its menu. The list reloads 0.5 s after the last device change of a burst, so a change within 2 s of another is no longer dropped.
  • A device that never answered (no identity yet) is listed and counted as Unknown, also after its polls failed; Offline means it answered once and stopped (device list, its counts, the dashboard and the live updates; the health tracker, alerts and bulk skips are unchanged).
  • Design v2, the device pages (port package P3). Every device tab shares the prototype's header: the name with its state, the address / chip / firmware line ("Update to x.y.z" when a newer release is cached; it opens New transaction with the device), the buttons Power (devices with one relay; it asks for the opposite state, never a blind toggle), Restart (a confirmation dialog), Back up, Edit and Delete (admins), the offline banner with "last seen", and the tabs Overview, Settings, Console, Backups, Drift (a tab now; was "Changes since the last backup" on the Backups page) and Replace device (a tab; was a button). The tabs no longer show the backup count; the trail names the device on every tab. The overview has the cards Identity, Power & energy (watts now, today / yesterday / total, a switch per relay), Network, Wi-Fi (with a signal quality bar) and Policy compliance (the device's deviations and "Make compliant…" for admins); hostname, gateway, SSID, BSSID and channel come from the newest backup snapshot. The state changes and transactions follow below the cards; the backup list is on the Backups tab only. The console is the prototype's dark panel: the history from the audit log, the reply of the command just sent as a line, quick commands Status 5, Power TOGGLE, Status 11, TelePeriod 300 and Restart 1 (sent at once and checked like a typed command), and Clear, which hides the lines shown so far (?since=). Settings is one form card with Reset and "Save to device"; a field whose value deviates from a policy rule says what the rule expects. Drift says how many settings changed since the last backup, "Show all" (?all=1) lists the unchanged ones too, with "Back up live state" and, for admins, "Restore backup…"; a reload within 15 s shows the settings read last instead of refusing. Edit device is one form card with Delete device…, Cancel and Save changes; title and trail keep the saved name while a refused form is corrected, and the failing field gets the focus. Breaking: POST <base>/devices/{id}/backup without from (the header's Back up) answers with a redirect to the device tab named by back and the flash "Backup taken." instead of the backup card fragment.
  • Design v2, the firmware page (port package P5). The releases are cards (version, "Latest", release date, how many devices run it); the newest three are listed, the others behind "Show N older releases". The selected release (?release=, the newest by default) shows its notes as an excerpt, "Release notes" (the whole notes and where the fleet stands, in a dialog; ?notes=1 without JavaScript), "Update N devices…" (devices that can take it) and the files this fleet needs with chip, devices, size and state: Cached, Prefetch per file (new on this page: POST <base>/firmware/fetch), Fetching… while a prefetch runs, "Download failed" with Retry. "Prefetch for my fleet" is a link above the files. The cached files are a list (file, release and size, short checksum, date, Delete in a dialog that says who still needs the file; a file a transaction needs links that transaction instead). Uploads are listed among them by their label; "Upload custom firmware" and the drop zone below the list open the upload form (a dropped file lands in it). The header says where releases come from and when they were last checked, with "Check for new releases now" (was "Refresh index"); its answer is a toast ("No release newer than x", "New release x found"), a failed check shows the reason and that cached files still work. The delete confirmation and the prefetch progress pages are restyled the same way. Breaking: the releases table, its Columns chooser and sort order, the "Prefetch a file" form and the cached-files table with checkboxes are gone; "Delete N unused…" replaces "Select unused" and the selection (same confirmation page), and the saved layouts and sorts of the firmware tables are no longer used.
  • Design v2, backups and replace (port package P10). The Backups tab lists one row per backup run (dump, snapshot and decoded copy together; the tab counts runs, not files) with the failed attempts among them (reason, Retry), the retention line ("Keeping the last 5 backups and anything kept · 3 will be pruned at the next run", "Change retention" for admins), the total, Back up now, and per row Newest / Kept / Pruned at next run, Keep, Download, Restore… and Delete (one dialog, confirmation page without JavaScript). Ticking rows shows the selection bar: Compare (two rows) and Delete selected (a confirmation page naming the count and whether the newest goes, GET <base>/devices/{id}/backups/delete). "Recent restores" stays below the list. The comparison (<base>/devices/{id}/backups/diff) has Older / Newer pickers, Side by side / Unified, "Show all N settings" (unchanged settings too, secrets never shown) and "Restore this backup…" (the older one); without parameters it compares the two newest backups, and two ticked rows (id twice) compare their runs. The restore confirmation shows what will change, the three steps and the one acknowledgement ("Restore and restart"); the progress page shows the four steps (backed up, uploaded, restarted, verified) with the failed one marked, and after a failure "Retry restore" plus, when it failed after the upload, "Restore the HH:MM backup" (the backup taken before). Replace device is a device tab: the new device from the discovered list (or typed in), what is always copied, a preview of what moves and what does not, then "Start replacement…" with the one acknowledgement. The device tabs gain Drift and Replace device (the header button is gone).
  • breaking: "Delete backups older than a date" is replaced by the prototype's "Prune now…": it deletes the runs the next pruning would remove (the ones marked "Pruned at next run"), from a dialog or GET <base>/devices/{id}/backups/prune (now without a date); POST <base>/devices/{id}/backups/prune is gone, the deletion goes through POST <base>/devices/{id}/backups/delete.
  • Spacing uses pixel-named tokens (--sp-N); the old --color-success/-warning/-info/-divider names, the generated colour ramps and --space-*/--radius-* are gone (no aliases).
  • Page padding follows the container width (clamp(12px, 2.4cqw, 32px)), 12 px on a phone.
  • Design v2, the dashboard (port package P1): "Fleet overview" with six stat tiles (Online, Offline, Unknown, Firmware behind, Policy deviations, New devices found), each a link to the filtered device list or to discovery, the fleet health bar, "Needs attention" (discovery proposals with Ignore / Add and Dismiss / Apply, then offline, never answered, refusing the login, silent and non-compliant devices, at most eight) and "Update transactions" (the newest as a card with its progress, the next three as rows, transactions named "TX-0001"). Buttons Add devices and New update; without devices the page shows only the empty state with Scan network, Add manually and Import from TasmoAdmin. The dashboard is live: its body reloads when a device changes state, a device is added or removed, or a transaction moves (the event stream sends a new state event only on a real state change).
  • The fleet counts (dashboard tiles, health bar and "Needs attention") call a device that never answered "Unknown" also after its first polls failed; "Offline" there means seen once, then lost.
  • Design v2, bulk actions (port package P7). Every action of the bulk bar (Send command, Back up, Restart, Power on, Power off) opens the prototype's scope page: the selected devices in the order of the list, each with its address and state, "Will be skipped" for the ones that are not online, "×" to take one out and "Change selection"; the options of the action in an aside with "N devices run · M skipped" and the run button. Send command: the command box, "Wait up to 5 s for each response" (checked by default) and "One device at a time", then the preview as before (the token binds command and devices). Back up: "Label (optional)" and "Keep these backups (never pruned)"; it starts at once. Restart: the scope page is the confirmation ("Relays switch off for a few seconds"), with "Devices at a time" (3, 1, 5) and "Save settings before restarting" (always on: Restart 1 saves). Power on / off: "All relays" or "Relay 1 only (POWER1)", relays already in the wanted state are left alone, and Power on can stagger the devices by 1 s. The result is a new page, GET <base>/devices/bulk/result/{batch} (admin), with Succeeded / Failed / Skipped tiles, a row per device with its answer, "Retry failed (N)", "Audit log" and "Back to devices"; it follows a running transaction live. Power on / off shows the same result page directly and a toast. Breaking: the bulk forms no longer take scope: the posted devices are the scope and "All devices" is gone from these pages (select all in the list instead); Power on / off asks first for a single device too; the restart scope page posts to /devices/bulk/restart/run directly (no "Continue" step); the backup form starts with run=1.
  • Design v2, sign-in, account and users (port package P9): the sign-in page, the forced password change and the short messages are the prototype's sign-in card (brand above, "TasmoFleet x.y.z · host" below); sign-in shows "session expired" and the attempts left after a wrong password, and "Keep me signed in on this device" is checked by default. The account page shows the profile of every signed-in user (name, e-mail, role, how they sign in) and, for local accounts, Change password and the profile form; the user menu and the phone drawer link it for everybody. The users page is the prototype's list (you first, then by name) with a "⋯" menu per user: every action is confirmed in a dialog, a forbidden one stays visible, disabled, with the reason, and while a step-up is due an action opens "Confirm it's you" first. Reset password generates a one-time password. The users list has no column chooser or sort links any more.
  • Error pages (design v2, P9): not found, not allowed and switched-off features are the prototype's error page in the app shell (code line, title, explanation, the address or setting, two ways on). A page above your role says which role it needs and what yours allows.
Fixed
  • A retry page with nothing to retry offers "Back to the transaction" instead of ending in a warning, and its breadcrumb is "Updates / TX-0007 / Retry" like the transaction's own page.
  • The device console wraps a long reply line (Tasmota JSON) inside the panel instead of running past its edge, where the end was cut off.
  • A partly read policy fleet no longer shows a bare 0: the dashboard's "Policy deviations" tile adds "n not checked yet" to the known count, and the device list's "Not compliant" chip shows "–" while nothing was read and "3+" (with the number of unchecked devices as its tooltip) while part of the fleet was.
  • The device list's MQTT · HTTP column is wide enough for both channel states by default (150 px, so Uptime still shows in full beside the pinned Power column at 1280).
  • /devices?health=offline marks its own "Offline" chip as the active filter (it removes the filter again) instead of showing "All" as selected over a narrowed list.
  • New update: the "Select" quick selection menu opens as a popover over the device list instead of in the flow of the "1 · Devices" head, which grew to the menu's height.
  • Text no longer breaks in the middle of a word when there is room: the stylesheet used word-break: break-all in 36 places and now uses overflow-wrap: anywhere (hyphens, spaces and slashes first, a cut inside a token only when it does not fit). File names, URLs, host:port and device JSON replies get break points of their own (<wbr>), so "tasmota-minimal.bin.gz", "mqtt.example.internal:1883" and a bulk or console reply no longer split inside "bin", the port or "Boot". A device name with no break point at all still wraps inside its box. A new layout guard (w) fails when a word of up to 200 px is cut over two lines.
  • Phone: the bulk scope rows have their padding back (the backups page's .bk-row rule had overridden them), the backup actions line up with the text above, the device card meta breaks between address, chip and version, discovery cards no longer show bare dashes, the sign-in footer is centred, the audit filters are two selects per row, the transaction list reads id, result, title, devices and actor in one order, the device picker of a new update gives names a line of their own, the style guide shows two stat tiles per row, the backup comparison fits without scrolling, a banner in a narrow card stays inside its border, replace-device checkboxes keep their size beside two-line labels, and a dialog that scrolls inside itself shows a shadow on the cut edge.
  • Phone: a scrolling tab row (device pages, Add devices, system settings) opens with the current tab in view (tabs.js) instead of cutting it off.
  • Replace device: the locked, checked boxes are readable in the light theme.
  • Audit log on a phone: rows have left padding and show the user and the target again.
  • Phone touch targets: the older-release chips on the firmware page and the theme switch in the drawer are 40 px.
  • Users: a user known only by a subject hash shows the address as its label; the hash is in a tooltip, not page text.
  • Back up / Send command / Restart scope pages: the option checkboxes ("Keep these backups", "Wait up to 5 s", "Save settings before restarting") no longer collapse into a one-word column that overlapped the "devices run" line (a backup-list class .bk-check was shadowing them). New layout guard o fails any text that runs out of its box or is drawn over other text.
  • Release-notes dialog on a phone keeps its title and Close in view and scrolls only the notes, with edge shadows showing that there is more; device settings' Save bar no longer squeezes its hint into a narrow column; New update's device names and the style guide's toasts break at hyphens and spaces instead of in the middle of words.
  • The dashboard's "Policy deviations" tile shows "–" ("not checked yet") instead of 0 while no policy value is known: live values are kept in memory only, so after a restart nothing is known until the first background read (spread over up to 10 minutes, none while the live check is paused) unless a decoded backup has the setting.
  • CI: TestEditorFieldsAreStateful (simulator, integration lane) no longer flakes with "connection refused" on /dl. Hostname, FullTopic and the MQTT settings queue a restart ~2 s after they change; when one of them came last in the (random) loop, the backup download could meet that reboot gap. The download now waits for the device to run again, as the commands already did. Test-only; no production change.
  • The progress bar of a transaction (ui.SegBar) leaves the devices still waiting empty instead of stretching the done, failed and running parts over the whole width.
  • System settings: "Reset to default on save" works with JavaScript on; the field's own value no longer makes the save fail with "a setting cannot be set and cleared at once" (P8).
  • An open dialog dims the whole window: the reserved scrollbar gutter stayed light (P8).
  • A typed choice between radio buttons (log level, sign-in method) survives a step-up: the draft kept the last radio of a group instead of the chosen one (P8).
  • The "device networks" link under a network scan's ranges opens the Network tab of the settings, where they are edited, instead of the first tab (?section= no longer exists; P8 review).
  • "Confirm it's you" still names the change after a wrong password ("Changing the sign-in settings") instead of an empty "Changing" (P8 review).
  • The "changed field" accent in the Settings forms was never drawn (it used an undefined colour name).
  • Dragging a column edge in the device list while a device event reloads the list no longer drops the resize: the drag goes on in the new table and the width is saved.
  • A push upload (/u2) whose image source ends early no longer sends the multipart closing boundary: the device answers on that boundary without waiting for the announced length and could have taken the cut image.
  • Simulator: a rebooting device stops accepting connections before it reports the reboot phase, as Tasmota does, and lets requests already in flight finish; before, a request in that gap was accepted and then cut.
  • UX polish from the design-v2 review (desktop): the audit log describes bulk backup and command starts in words ("2 devices · 1 left out") instead of keep=false label=; a subject nobody recorded reads "Unknown user" everywhere, not "user 9f86d081"; retention periods are written as "30 days" (the audit retention hint says what 8760h is); the "Release notes" heading no longer doubles the notes' own "Changelog"; the upstream link of a release is "Upstream release page" (the card has an own "Release page" link); "Newer than this release (0): none" has its space; the artifact delete page explains why it lists more devices than "still need this file"; the device settings selects use one "0 (off)" style; "Web user" everywhere.
  • Breadcrumbs and navigation: the delete-backup page names the device, "Nothing to prune" leads back to the backups, a missing device or transaction keeps Devices or Updates highlighted, and the answer to "Run now" no longer shows "Policy / Policy".
  • A bulk run page says "running for" (not "took") while it runs, and a device row that is still running shows its step, not the finished reply.
  • Layout: the device overview cards stack in columns without holes, the drift action bar reaches the page edge and Restore is primary only when a setting changed, the edit device form is as wide as the add form, the policy rule editor stacks Setting and Expected value, the discovery table gives the MAC column its width, the transactions history has column headers, the Options of a new update line up in one column, the console's input placeholder is readable in light mode, and a view-only Users list shows a Details link instead of a menu of disabled actions.
Breaking
  • The policy rules page <base>/policy/rules is gone (no redirect): the rules are edited on <base>/policy. The export moved to <base>/policy/io/export, the import to POST <base>/policy/io/import; <base>/policy/rules/diff is replaced by <base>/policy?v=<id>. The ?status= filter of the policy page is gone with its device table. (W1)
  • The account page moved from <base>/account/password to <base>/account (no redirect); the password form still posts to <base>/account/password. A user who must change the password is sent to <base>/account.
Security
  • Accepting a discovery proposal with the fleet default device password needs a host that proves the proposal's MAC without credentials; otherwise the accept is refused (accept without the default and set the password in the device edit form). The default password no longer goes to an address only a sighting vouched for.
  • The setup token (setup step 1) is checked and spent in one step, so two concurrent submits with the same link cannot both pass; a form error gives the link back. The setup page sends Referrer-Policy: no-referrer, so the token in its URL is not passed on in Referer headers.
  • The setup link (setup step 1) only works for the admin account the first start created. An account created later under the same name (by an admin with a one-time password, or by the CLI with a forced password change) also waits for its first password; the link already in the container log, or a fresh one printed at the next start, could set that account's password and sign in as admin. The first-start account now carries a stored mark (migration 0025) that only the first start sets and every password change, reset or disable clears; the creator field is not used for this, since an SSO or proxy subject can be any string. Accounts that existed before this release do not get a setup link; they sign in with the generated password as before.

0.10.1

Fixed
  • Row "Actions" menus work again in tables with a saved column layout (desktop). A saved layout switches the table to fixed column widths, and its cells cut off what does not fit: the Actions button was cut in half and the opened menu was hidden inside its cell, so a click showed nothing. The row-actions cell no longer cuts off its menu, and the checkbox, Power and Actions columns are never narrower than their controls at any window width, whatever share the saved layout gives them (the other columns share the rest). Present since 0.9.0.
  • Switching pages no longer makes the page jump or flash. Since 0.10.0 (no reserved scrollbar gutter) a page with a scrollbar laid out 15 px narrower than one without, so the top bar and the page moved on every switch between a short and a long page (main pages and device tabs alike). The scrollbar gutter is reserved on every page again; the price is the empty strip right of the top bar on short pages with classic scrollbars, which 0.10.0 had removed. The Archivo fonts were the only static files without the version hash: every page revalidated them and painted in the fallback font first. The stylesheet now links them with the hash (cached immutable, the stylesheet's own hash follows its fonts), and the page preloads the font.

0.10.0

Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • Design system spec, Deviation 7: all pages are ported and knownLayoutIssues is empty; the deviation now lists the pages that still use the legacy CSS block.
  • Stylesheet clean-up, no visible change: the old app.css token aliases (--bg, --muted, --brand, --overlay, ...) are gone and every page rule uses the design-system tokens, and the unused .subnav rules are removed. The "page rules" block stays: its remaining rules are all in use. A test keeps the aliases and .subnav from coming back.
  • Updates and transactions polish. The update preview counts refused devices as "N refused" (red) instead of hiding them in "skipped", its phone rows are labelled lines (Plan, File, Power restore, Note) with no bare "-" and no power-restore line for devices that are not updated, and the push refusal for an ESP8266 no longer quotes an internal backlog id. New transaction warns, as soon as Push upload and an ESP8266 (or unknown) device are both chosen, that those devices would be refused. The selected-row highlight and accent bar of the picker are painted by the whole row on a phone. A transaction's target, starter, start time and duration are labelled facts in the live summary, so the duration keeps counting; logs opened with "Expand all logs" stay open while the cards refresh and the button says what the next click does; a job whose cancel is pending shows "cancel requested", loses its Cancel button, and the log reads "Cancelled by adam" instead of "Cancel cancelled by adam". The bulk Send command preview names the consequence of Restart 1 and its button says "Restart N devices". The retry form uses the full card width. Two-column tables (Device, Result) have no Columns chooser.
  • One section component: a folded section (Firmware Prefetch a file and Custom images, the Setup steps and any other closed card fold) keeps the card frame and its legend title instead of turning into a bare 2px rule, so every section on Firmware, Policy and the rest looks alike.
  • Without JavaScript, "Delete backup", "Delete rule", "Cancel transaction" and "Cancel update" now open a confirmation page (the same wording and the same POST as the dialog) instead of doing nothing, like Delete and Restart device. With JavaScript the dialogs open as before.
  • Users, System settings and Audit polish. Users: the row Actions menu holds action items only ("Make admin" / "Make operator", a labelled new-password field, Disable, Delete); your own row says "You" instead of an empty cell; the State chip and its note read as one value; long names wrap; the table keeps its Columns chooser and sorting before step-up; the "Proxy"/"SSO" chip no longer repeats the Method column; the New local user form has the same width as the other forms; the step-up text names creating users. Change password has a breadcrumb trail and no repeated card title. System settings: Save returns to the tab that was saved, the Network tab has its legend, settings set by the environment use the label pattern of the editable ones, the read-only table has no Columns chooser, and "Device names in metric labels" sits in a Metrics group instead of Audit. Audit: local users carry a "Local" chip like the other kinds, the MQTT and SSO tests have readable names, an actor nobody recorded reads "Unknown user", and "Clear the filter" appears once and is a 40 px button on a phone.
  • The style guide shows every overlay open and in the flow (top bar group and user menu, phone menu, row Actions menu with its danger item and the upward up variant, Columns chooser, dialog), the device tabs from the real devtabs partial with their phone select and a disabled tab, and the real bulk bar actions (new bulkactions partial, shared with the device list) once inline and once folded into the Actions menu. Static examples carry data-static, which the menu, tooltip and table scripts ignore.
  • The dashboard keeps its "Recent transactions" card when there are none: "No transactions yet.", with "New transaction" for admins. Its table has no Columns chooser any more.
  • Form errors read as sentences and sit at the input they are about: the sentence under the input, the input marked invalid (border, aria-invalid, aria-describedby) and focused, no repeated page banner. One shared pattern (fieldError / fieldInvalid template helpers, documented on the style guide and in the design system) now covers the password change and forced-change pages, the Users page (create keeps the typed username and role, shows the 12-character hint; a wrong step-up password marks its input), the policy rule editor (a save without a setting says "Choose a setting." once, at the Setting select, instead of policy rules: rule 6: invalid command), the device form (name, address, port, tags, notes), and the System settings values.
  • Device pages share one header (name, state badge, Replace device, trail) on Overview, Settings, Console and Backups; the tab names the section, so "Settings of ..." and "Backups of ..." headings are gone. A device that never answered or is held by a running operation has "Back up now" disabled with the reason next to it, the Transactions card is always there (with an empty state), and the Overview says why nothing works yet. The Identity card is swapped as a whole by "Refresh identity": a failed probe is the error callout (a sentence, not a Go error chain) and keeps the button, a good one updates the rows. When the Settings cannot be read the page offers Try again and Back to the overview, and the Settings tab keeps the current-tab style.
  • Backups tab: with no backup the compare, "Changes since the last backup" and prune controls are gone and the empty state has a "Back up now" button; config dumps get a Note column; restore jobs show their state as a badge. The delete dialog of the newest backup uses the danger callout in sentence case. Restart confirmation uses the primary button (red is for delete). An unchecked chip family on Restore and Replace is a warning, not an error. Replace no longer shows the "old device still answers" message twice.
  • Phone: drift, comparison and replace tables keep their Before / After and Old / New labels; the Console reply keeps its tokens whole and scrolls sideways. Replace: select, row headers, row rules and card gap fixed; Settings: the General card lines up with Options. The MQTT row no longer repeats its own label.
  • A failed manual backup says what to try ("The device did not answer ...") instead of showing the Go error chain; the details go to the server log. "Select at least one device." starts with a capital letter and ends with a full stop.
  • Dialogs: opening one from a row's Actions menu closes the menu behind it. The dialog of a running transaction or job names its dismiss button "Keep running" instead of a second "Cancel". The row menu's "Restart" reads "Restart..." like "Delete...", and the style guide shows it. "Expand all logs" is a secondary button, not a link-looking ghost button.
  • Add devices, manual tab: a failed probe says what is wrong in a sentence under the field it is about (Address, or the web password for a device that asks for one) instead of the Go error chain ("Probe failed: tasmota: address outside ..."). "Add without probing" is offered only where it can work (not for an address outside the device networks, which the inventory refuses anyway). The button reads "Add device", and it is disabled, with its reason, while no device network is configured, like Start scan.
  • Add devices, scan tab: a refused range ("10.99.0.0/24 is outside the device networks.") sits under the Ranges field, which is marked invalid, and the proposals table keeps its sort links after the failed request. "Show ignored" is a filter above the table (checkbox plus Filter) instead of a link below it. Ignore and Add device stand side by side. A finished scan says "Nothing found.", not "Nothing found so far."; with no device network the page no longer says "Start a scan" and names the setup once. The scan history is a table instead of a bulleted list.
  • Add devices, import: the plan counts rejected lines as skipped and marks a row whose address is outside the device networks as skipped, with the reason, instead of listing it as Create and failing on apply. With nothing to apply the page says "Nothing to import." and offers no "Apply import". The result uses the plan's badges (failed in the danger tone), keeps host:port in Address and gives each failure one sentence instead of the inventory error chain. The info button of the key file sits inline after its label, and so does the size hint of devices.csv. A row that only has a warning (unusable port, password without key) stays planned and is no longer counted as skipped as well; a failed import line no longer shows the update hint. The scan Ranges help links to the device networks only when one is set.
Fixed
  • Long unbreakable text (a device name like AtticHeaterPlugWith...NeverHasABreakAtAll, an error chain) wraps inside its box instead of widening the page. One shared rule covers the device header and its back link, the retry form's "Last result" text, the bulk back up / restart scope list, the row dialogs (title, text and Close stay inside the frame), and the firmware release dialog. Device names on phone cards and in firmware lists wrap in full instead of being cut or ellipsized. An e2e test opens the key pages at 375 px with such a name and fails on any sideways scroll.
  • Pre-v0.10.0 recheck regressions. The rule preview callout in the policy editor keeps room for its icon on a phone instead of drawing it over the first word. A row Actions menu opens up or down by the space in the viewport when it opens (a short Users list no longer opens upward over the top bar); the server's "last two rows up" stays as the fallback without JavaScript. The bulk Back up and Restart scope choice has a legend ("Devices") like Send command. The transaction header (state, "n of m done", counts, duration, Cancel) re-reads itself every 3 seconds while the transaction is active, like the job cards, instead of waiting for a throttled event. Logs opened with "Expand all logs" stay open when one card is swapped twice at once.
  • Policy pages (QA v0.9.0): on a phone a rule card has the rule name as title, the setting as subtitle and labelled lines (Expected, Applies to, Severity, Findings); the Note of the rule editor has a visible label at every width; the import check result (or refusal) sits inside the Import JSON section; the rule preview is green only when every device was judged and none would be flagged; "Add rule" is not a second primary button while the editor is open; Policy rules uses the same breadcrumb root as Make compliant (Devices > Policy > ...); the version diff heading is short enough for a phone; an empty rules or versions list shows no table header, "0 deviating" is neutral and Check now is disabled without rules or devices; the Source column stays on one line and "checked ... ago (live)" is shown once on a desktop; Make compliant labels "Commands" and "Not changed" on a phone and its footer stays inside the card.
  • Phone folds on a desktop (QA v0.9.0): section titles that are phone folds (Setup steps 2 to 4, Policy Rules, Import JSON) are plain framed card titles from 48rem up, without a chevron and without the unframed look of a closed fold; closed card folds on a phone are at least 40 px apart.
  • Standalone pages (sign-in, forced password change, access denied) follow the system dark mode: their <html> lacked the data-theme-auto marker. The sign-in button of the SSO link now has the width of the form buttons, and the forced password change names the signed-in account and aligns its "Sign out" button.
  • The sign-in page says why it appeared ("You have been signed out." / "Sign in to continue.").
  • "Access denied" names the role the page needs and the one the account has; an account without any role gets a standalone page instead of an app shell whose links lead back to the denial; that page carries a "Sign out" button so the account can be switched.
  • A missing device or transaction shows "Device not found" / "Transaction not found" with a link to the list; the "switched off" pages make "Open the system settings" the primary action for admins and show FIRMWARE_BASE_URL as code. Only a missing device or transaction says so: a missing backup of an existing device gets the generic "Not found" page.
  • CI: the mqttbus tests no longer flake ("timeout waiting for message", e.g. TestNonDefaultFullTopic). The test helper returned once the connection was open, before the broker had processed the bus's SUBSCRIBE, so messages published right after were lost on a loaded runner. It now waits for the broker to hold the subscriptions. Test-only; no production change.
  • CI: the phone height check of the policy page no longer fails by a few pixels on a slow runner. The background policy reads start at random times after the app starts, and each deviating device they found made the page taller; the check now runs with those reads off and measures the unchecked page its budget is set for. Test-only; no production change.
  • Tables keep their sort links and layout on the page a POST answers with (Firmware after "Refresh index" or a failed fetch, the policy rules after a refused save, discovery after a bad scan range). A table with no row shows no header, select-all box, sort links or Columns chooser; the confirmation table of "Delete selected files" and the key/value tables have no Columns chooser, and the confirmation row leads with the file name.
  • Firmware: the folded "Older releases" table sorts and is laid out like the newest releases (one Columns chooser); its meta counts the releases a device can still be updated to ("7, none needed by this fleet" instead of "7, 7 needed"). Device names in the release dialog, the release detail and "Used by" never break inside a name (a name longer than the line is cut with an ellipsis and has its full text as a title) and look like the links in the tables. The "recommended" badge is the same in the table and the dialog, a file held by a transaction says "Transaction #N" and its disabled checkbox stays visible, and the release and chip line of a cached file stays on one run.
  • The top bar, its rule and the dialog backdrop reached only 15 px short of the right edge on pages without a scrollbar (reserved scrollbar gutter, removed).
  • The flash message no longer covers the top bar and the menu button.
  • Phone menu: "Sign out" is left-aligned like the other rows, the signed-in user's name keeps its case, and the last row has room below it.
  • Text such as "HTTP scan" no longer loses the space after a capital letter (font kerning).
  • Dashboard zero tiles and the sort icons in table headers are readable (contrast); the select-all box lines up with the row boxes; a stat card's title no longer draws a card-coloured box above its rule.
  • The divider above the buttons of a confirmation page ended 4 px outside the card on phones.
  • An action row or form right below a table has a gap; a checkbox label that wraps keeps its box on the first line; the device health line no longer breaks inside "wifi 36%".
  • Native controls follow the flat design: progress bars (scan, prefetch, restore) are full width with the accent colour, job progress bars and rule chips are square, job cards have no shadow, a failed job's bar is a full danger bar, file inputs have a button like the others, and an unticked confirmation checkbox is outlined instead of struck by a red bar.

0.9.0

Added
  • Devices: bulk "Back up" and "Restart" (admin) for the selected devices, or all (chosen explicitly, never implied). Offline devices are left out and listed. Both run as a transaction like "Send command": a result per device, "Retry failed", audit entries (backup.bulk.start, command.bulk.start). A backup starts at once; a restart sends Restart 1 through the command service (policy, device lock, audit) after one confirmation that says the devices go offline briefly.
  • Experimental: not yet tested on a real device; default delivery stays pull; ESP8266 refused. OTA push upload as an optional delivery per transaction (update form, "More options", "Delivery"): tasmofleet uploads the image to the device's /u2 instead of the device downloading it. ESP32 only; ESP8266 is refused with the reason until it was tested on real hardware (F-9). Guard runs before the upload, a lost reply is probed and never resent, and the device's OtaUrl is not touched. Default stays the download. New migration 0020.
  • New design (the nav-patterns port of the Claude Design system): flat Modernist look with local Archivo fonts, light and dark themes that follow the system ("Auto"), a phone menu panel, breadcrumb trail, device tabs (Overview, Settings, Console, Backups), one Actions menu per table row, a fixed bulk bar with a "Clear selection" button, and a "Columns" button label. Applies to the Dashboard, Devices, Device detail and Firmware pages, and to System settings, Audit, Users, About, Setup, Login, Change password and the error pages; the other pages follow in the same release. Binding reference: docs/spec/design-system.md and the admin page /styleguide (every component and state, light and dark side by side).
  • Devices: "Restart" per device (admin) in the row menu. It sends Restart 1 through the same command service as the console (policy, device lock, audit) after one confirmation.
  • Devices: a confirmation page for "Delete..." (/devices/{id}/delete), the no-JavaScript fallback of the dialog. Firmware: "Start update..." in a release's menu opens the update form with that release selected.
  • Per-device Prometheus gauges while "Device labels in metrics" is on: tasmofleet_device_up, _uptime_seconds, _wifi_rssi_percent, _wifi_signal_dbm, _power_watts, _energy_total_kwh, _energy_today_kwh, _energy_yesterday_kwh, _relay_on and _last_seen_timestamp_seconds, labelled device and id (plus channel or relay). They come from the health cache, never from a device query during a scrape. The Grafana dashboard has a panel row for them; see docs/metrics.md.
  • Devices: bulk "Send command..." (admin) for the selected devices, or all when none is selected. One command line, a Backlog included; the preview lists who gets it and who is left out (offline), names the consequence of destructive commands, and the send runs as a transaction with a result per device, "Retry failed" and an audit trail. The scope is chosen explicitly ("Selected devices" or "All devices", nothing preselected). Credential commands are not sent in bulk.
  • Settings: "Keep the stored password if the broker changes" for the MQTT password.
Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • System settings: seven tabs (Network, MQTT, Firmware, Discovery, Backups, Sign-in, Advanced) instead of a side list; "Test MQTT" and "Test discovery" are buttons of the settings form (they use only the broker or issuer fields and never save anything); the container-only settings are a fold in Advanced. Old ?section= slugs fall back to the first tab.
  • Audit: details open inline in a log box instead of a tooltip, "Clear the filter" sits in the filter bar, an empty result shows an empty state, and the pager is the same at every width (Previous, position, Next, page sizes; no First/Last/page numbers).
  • Users: "Manage" is now "Actions"; proxy-header accounts are shown as display-only rows ("Managed by the proxy"); the step-up text mentions ending sessions only where it is offered.
  • Setup: step 4 explains the prefilled networks and "Finish the setup" follows the last step.
  • The forced password change has a "Sign out" button.
  • Sign out moved into the account menu of the top bar. Proxy-header accounts show their name as plain text with a Sign out button. Updates menu: Transactions, New transaction, Firmware.
  • Firmware page: "Refresh index" is the page action; the three newest releases are listed, the rest sit in an "Older releases" fold; the cached-files table drops the Release, Chip and SHA-256 columns (release and chip are under the file name). Delete confirmations use dialogs.
  • The device list no longer reloads while a row menu or dialog is open or devices are selected.
  • Design port, second part: the device pages (Settings, Console, Backups, Restore, Replace, Delete old backups, Changes), Add devices (scan, import), Policy, Policy rules, Make compliant, New transaction, Preview, Transactions, Transaction detail, Retry, Switch on/off and Send command use the new patterns: breadcrumb trail, device tabs, one Actions menu per row, the shared bulk bar, confirmation cards, dialogs instead of browser confirms (cancel transaction, cancel update, delete backup, delete rule). Policy: "Check now" is the page action, "Select all deviating" plus the bulk bar's "Make compliant" replace "Make all compliant" and "Make selected compliant". The update picker's "Select none" is the bulk bar's "Clear selection". The console history is a log box.
  • Folds that hold what people come for (backups, state changes, transactions, cached files, policy rules) are open on a desktop and closed on a phone; secondary ones stay closed.
  • Tables with fewer than two rows get no Columns button.
  • Console and command policy ("admins may do everything", 2026-10-21): an admin can send any Tasmota command: no allowlist, no commands that are refused for everybody (Backlog, Rule, Timer, Script, sensor and driver commands, credential commands, SetOption85, Reset, WifiConfig, Upgrade, OtaUrl, ...). Destructive and lock-out commands (Reset, WifiConfig, firmware, Wi-Fi off, WebServer other than 2, IPAddress, Ethernet, Ap, Module, Template, credential commands, Restart other than 1) ask once and name the consequence; routine commands no longer ask. The confirmation does not need the command typed again. A manual Status 0 is allowed with a warning; tasmofleet's own code still never sends it. Operators stay at read, power and Restart 1.
  • TasmoAdmin web import needs no step-up and is available in header mode; the file names and counts of every check are audited.
  • Changing the address or port of a device with a stored password is allowed for admins: the edit form warns that the password goes to the new address. Discovery offers "Update address" for such devices. An answer without the password is no proof of identity for a password-protected device, so the move always needs a second confirmation for exactly that address, which is audited as "MAC unverified"; another MAC stops it. The scan, MQTT announcements and imports still only propose.
Breaking
  • TASMOFLEET_IMPORT_WEB_HEADER_MODE is removed (no shim); the web import is always on.
  • Backlog, executors and credential commands are no longer refused: rely on the admin role, not on the policy, to keep them away from people.
Security
  • Credential arguments are never logged: audit, history, transaction pages and errors show Name ***; a Backlog is logged part by part with each part redacted.
  • A command transaction is sent as its creator's current role, checked before each step: a demoted, disabled or removed creator fails the step (audited); a retry is admin-only and runs as the retrying admin. A step found mid-send after a restart is not sent again.
Fixed
  • Devices: bulk "Back up" and "Restart..." are now buttons in the bulk bar and its Actions menu (admin, with the feature flags). The scope page pre-selects the picked devices and names them, and the backup result links "Transaction #N" as the primary action and lists the left-out devices with their reason (they are not part of the transaction).
  • Policy: with a device's Details open the list no longer scrolls sideways on a phone; the findings wrap inside the card.
  • Device page: a running backup says "A backup is running" instead of "being updated"; the retry page of a backup transaction has its own text and keeps the failure reason visible on a phone; long device names wrap on the bulk restart confirmation.
  • Health: devices added by another process (for example tasmofleet import while the server runs) now show live state within one 5 s tick instead of after up to 30 s; the tracker checks SQLite PRAGMA data_version per tick and reloads the inventory only when it changed.
  • Tables: a live list update no longer undoes a column width set a moment earlier (the save was still in flight) and keeps the keyboard focus on the resize handle.
  • Tables: a live list update that arrives right after a resize handle got the keyboard focus, before the first arrow key, no longer drops the focus.
  • Devices: the list no longer freezes while devices are selected: a live update is applied and the selection and bulk bar count are restored; with a row menu or dialog open the update waits until it closes.
  • System settings and Setup: a connection test that ran and failed (MQTT, SSO discovery) is shown in the error notice, not the green success one.
  • Device settings: Apply without a change shows "Nothing changed." as a neutral notice (HTTP 200, not 422). The form that comes back is read from the device again, so fields the device did not report stay "not reported" and the change counter starts at zero.
  • Device console history: the "Proxy" / "SSO" tag no longer overlaps the actor name (the hanging indent of the log was inherited into the tag).
  • Device Backups tab: the page lists the snapshots (new "Snapshots" section) and the tab count is config dumps plus snapshots, so count and list agree.
  • Phone row cards no longer hide what identifies a row: a discovery proposal keeps its name on the first line and shows MAC, topic, source and seen as labelled lines with Ignore and Add device on their own line; policy device names wrap instead of being clipped; cached firmware files show "Installed on" and "Needed by" before you delete one.
  • Transaction pages: a job card no longer sets the page width. The one-line log summary (a whole Status 2 reply) and the status line are ellipsized inside the card, so Cancel, Retry and the badges stay on screen at 1280 and 375 px.
  • Devices list: a long device name wraps inside its cell instead of pushing the Actions column past the content edge; device chips in the rule editor wrap long names too.

0.8.1

Fixed
  • Visual QA of v0.8.x, shared components and the dashboard, firmware, update and transaction pages:
    • Menus, row menus, the Columns panel, tooltips and dialogs have one border (3:1 against the page) and one shadow that shows in the dark theme as well.
    • The automatic tooltip no longer copies the text of a menu or a button row into a bubble over the open menu, and the "Show or hide columns" bubble stays away from the menu it opened.
    • Cards keep their content 0.75rem below the title on the frame. On a phone a collapsed section is one header row with the chevron at the right, not an empty frame under a title; the chevron sits on the middle of the line (folds, About, transaction logs).
    • Row actions and the "Show older releases" switch stay inside their rows and centred; "Delete..." no longer covers the size of a cached file; checkboxes sit on the middle of their row; the Dashboard link of the phone top bar is centred like the other items.
    • Phone cards print no label line for an empty cell. The page scrollbar is always painted, so bars and backdrops reach the right edge.
    • Dashboard "Recent transactions": one row per transaction (target, device summary, state, started by) instead of one row per device; the target is the release alone.
    • Firmware: fleet counts say "N of M known devices can be updated (K unknown)"; unknown hardware is never "to update"; every release shows a figure that is true for it ("22 to update", "3 on this release", "5 older", "all 26 newer"); long changelog links wrap instead of widening the page; the Cached files table fits its card; lists of devices never start a line with a comma.
    • Update preview: a skipped device is neutral and not an error, "0 to update" has no success tick, and the power restore line no longer contradicts itself. The retry page words "nothing to retry" as a warning. Breadcrumbs sit under the title on every page.
    • Transaction page: the meta line breaks between its parts, queued jobs have a label on their log fold, device names are no longer cut in the middle of a word, the Cancel buttons of the phone cards line up, a log box shows whole lines.
  • e2e: layout guards for the whole signed-in UI (controls inside their row, no overlaps, centred labels, no empty collapsed frames, equal button heights, legend spacing, delimited overlays) run in ui-pages.spec.ts at 375 and 1280, light and dark.
  • Visual QA of v0.8.x, part B: the device, policy, audit, user, system and setup pages:
    • A path that does not exist, a 403 and the update pages of an installation without a firmware base URL answer with the app's own page (navigation, "Go to the dashboard", an explanation) instead of Go's plain "404 page not found". The "Import from TasmoAdmin" tab is hidden when the web import is off (header mode).
    • Devices: long names are cut with an ellipsis and never run under the power toggle at 375; an empty inventory says "No devices yet" and offers Add devices; a filter without a match offers "Clear the filters"; an empty list is no table, so the header no longer stops short of the card edge; "Select all" is disabled with no rows.
    • Device detail: the Backups table has a real Actions column; on a phone a backup card keeps its note and puts Download, Restore and Delete on a line of its own; the header buttons share the width; "Compare and restore" is a button. "decoder outdated" is explained, the "Pick a date" page has a form, and the drift page offers "Try again".
    • Restore, replace and delete-old-backups: the confirmation box shares a line with its sentence, a missing tick is shown at the box, advisories start with a capital, a running job is no green success box and has a progress bar, the "Open the device" link no longer wraps in a column.
    • Policy and rules: Note folds and status folds no longer overlap their neighbours at 375, rule cards show their findings, names are cut with an ellipsis, a device with unknown values says so next to "N to fix", the empty rule table says "No rules yet", the doubled parentheses are gone, the editor's device list shows that it scrolls.
    • Audit: full timestamps and device names fit at 1280; the phone cards keep their user and target readable; an empty filter result offers "Clear the filter" and has no pager.
    • Users: the table uses the page width, "Last login" says "ago", the Actions column and its label appear only when actions are unlocked, and the step-up text lists what it unlocks.
    • System settings, setup, About: the save hint has room beside the button, the setup steps say "(optional)", the Finish button is outside step 4, and the effective configuration prints lists and durations for people and tells unset from masked values.
    • Second QA pass: disabled buttons no longer show an endless busy spinner (only a running request does); "Switch on/off selected" is off without a selection; the per-release Fleet cell on Firmware names unknown devices ("3 to update · 7 unknown"); the drift throttle notice is info, not a success box; policy rule rows no longer jump when their menu opens and version notes stay readable at 375; menu entries are 40px touch targets; "Old credentials come back" and "The old device must stay switched off" are warnings.
    • Users: an admin can no longer disable, delete or lower their own local account (the server refuses, audited as denied; the own row has no Manage menu). Demoting the last admin stays forbidden.

0.8.0

Added
  • Users page (/users, System menu): every principal in one list. Local accounts and the SSO and proxy-header users seen at sign-in, each with display name (with the anti-spoofing suffix when names clash), method, role (for SSO the role of the last sign-in, which still comes from the IdP groups), last login and live sessions. The list needs no step-up; only changes do. Local accounts are managed on the same page (create, role, reset password, disable, enable, delete). SSO users get "End sessions", which signs out exactly that user. OIDC admins confirm with the IdP step-up and may now manage local accounts; header admins have no step-up and see the page view only (migration 0016 principal_logins: role and last login per principal).
  • Replace device: after a hardware swap, a wizard starting from the dead entry puts its newest (or a chosen) backup onto new hardware and lets the entry take over the new MACs in one transaction. Candidates are devices that are online, inside the device networks and bound to no entry; the chip family must match (final), a firmware major change needs a tick, the preview shows what the dump sets (hostname, static IP, MQTT topic and client, Wi-Fi names, template), and it refuses while the old device still answers. The new hardware is backed up first; name, tags, notes and history stay, the old MAC goes into the audit (migration 0017 replace_device). See docs/security/restore.md (R18-R22).
  • Backups: delete a single backup (admin, confirmation, audited; the device's newest backup gets an extra warning) and "Delete older than..." per device with a preview of what goes. Restore is a button in the backup list and on the device page.
  • Policy page: "Make compliant" per device and "Make all / selected compliant" (admins only). A preview lists the exact commands per device. Only deviations whose value was read live become commands; unknown and backup-only values are listed and skipped, and the ESP32 exception of the rules applies as on the page. No confirmation tick: preview, then Apply. It runs as a transaction like an update (progress per device, "Retry failed", the parallelism setting): the commands go to each device batched with Backlog, followed by exactly one SaveData (none extra when the batch contains SaveData or a setting that restarts the device, which saves by itself), no restart otherwise. Afterwards the values are read back and the policy page shows the new state. Devices that are locked or have a running job are skipped with the reason. Every change is audited (policy.fix.start, policy.fix). Needs the same job engine as updates, so it is available when the firmware server is configured. Migration 0018 policy_jobs (job kind and commands).
Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • Restore needs no password step-up any more. One confirmation names every risk (restart, replaced configuration, old credentials, firmware major change, unchecked chip family); the live checks run before the page is shown, and a refused restore shows no form. breaking for scripts: POST /backups/{id}/restore/stepup is gone.
  • Restoring a device whose MAC was never stored reads the live MAC and stores it instead of refusing.
Breaking
  • /admin/users moved to /users, the account actions to /users/local/{name} and /users/stepup; the old routes are gone without a redirect.
Security
  • Local account management is no longer restricted to local admins: an OIDC admin who re-authenticated at the IdP (5 minute step-up) may manage local accounts. Every change still needs a step-up, the "last admin" rule and the forced password change are unchanged, and header principals cannot change anything.

0.7.0

Added
  • Custom firmware: admins upload their own .bin or .bin.gz on the Firmware page (label, numeric version, build name). The chip family comes from the image header; safeboot, factory and full-flash images, unknown chips, corrupt files and files over 4 MiB (ESP8266) or 8 MiB (ESP32) are refused. An upload is identified by its SHA-256, so it may carry the same version as an upstream release (devices fetch it from its own URL); the same file twice is refused. Uploads appear on the update page as "custom: label (version)", never preselected, and pass the same chip-family guard; ESP8266 uploads are staged through a minimal image borrowed from upstream. Upload, refusal, use and delete are audited (migration 0014 firmware_custom, target_custom on jobs). See docs/security/custom-firmware.md.
  • Sorting for the remaining tables: transactions, firmware releases and cached files, policy devices, settings, rules and versions, discovery and users. Typed keys (versions as versions, addresses as addresses, times as times), a header link per column, the choice saved per user like on devices and audit.
  • System menu: "Users" entry for admins.
  • (i) help: long explanations move into a tooltip next to a short label or intro line.
Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • One page header on every signed-in page: the title row is as tall as a button, so the title no longer moves between pages; the way back (crumb) sits under the title.
  • One card component: the title sits in the frame (like a fieldset legend) on every card; plain sections of the dashboard, setup, backups and users are cards now.
  • Badges: the icon has its own space before the text everywhere.
  • Tooltips explain and never list data: at most 160 characters (also enforced on the server by the tip template function for titles that carry data), one or two short sentences; the "Refresh identity" button lost its tooltip.
  • Shorter intro lines and labels with the rest in an (i) tooltip (transactions, console, import, firmware prefetch and delete, retry, setup, device form, about).
  • Device settings page: full width, one card per group in two columns from 72rem, a compact label/value grid with the help in an (i) tooltip, sticky jump links and Apply bar. The intro is "Applied immediately and saved to the device once."
  • Update options: one select "Power after update" (Restore lights, Restore lights and relays, Leave as is) replaces the two power checkboxes, so the meaningless combination cannot be sent. The variant override and the discovery address change no longer need a confirmation checkbox (the preview shows the variant, the server probes the MAC itself).
  • Cached files: row checkboxes with "Select unused" and "Delete selected" replace "Delete all unused"; the device column is now "Installed on" (informational) and the new "Needed by" names the pending or running transactions. A needed file cannot be selected, and the server refuses it too. Uploads are never part of a bulk delete. Breaking: the routes GET/POST /firmware/unused/delete are gone (POST /firmware/delete-selected replaces them).
  • Audit log: Result is a status badge only (ok, failed, refused); the new Summary column (at most 40 characters, full text in the tooltip) is written per action type and never repeats Action or Target; actions and targets read as names. Starting a transaction is audited (transaction.start).
  • Policy page: summary chips (deviating, compliant, not checked) at the top filter the device table; then Rules, then Devices. A device whose only issue is a value that could not be read counts as "not checked", not "to fix".
  • Page intros are one line and labels at most six words on the settings, update, firmware, audit, policy, system settings and discovery pages; the explanation moved into (i) tooltips.
  • System settings: the button name and the scopes no longer need a step-up; everything that decides who signs in or with which rights still does. Values typed before a step-up come back afterwards.
  • Principals: users are shown by the display name captured at sign-in ("alice" with an SSO badge, "admin (local)", system actors with their own badge, the full subject in the tooltip) in the audit log, the console history, transactions, restores, scans, prefetch runs, custom uploads and policy versions. Names that two users share, or that look like a system actor or a local account, are shown with the start of the subject; the audit user filter accepts the name and asks when it is ambiguous (migration 0015 principals).
Security
  • A user cannot pass as "System", "Discovery" or "admin (local)" in the audit log by choosing that display name: such names are always shown with a subject suffix (docs/security/system-settings.md, docs/security/discovery.md mark the superseded rules).
Fixed
  • Import page: the key-file label points at its file field again, not at the (i) button.

0.6.2

Added
  • Discovered list: "Show ignored" with "Un-ignore" for dismissed devices.
Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • Identity rule (spec 11.1): a device is identified by its set of MACs (Wi-Fi plus Ethernet where present; migration 0013 device_macs, seeded from the existing MAC). Discovery, the MQTT known-device index, import, restore and OTA checks match any MAC of the set. No automatic path changes the set.
Fixed
  • Discovery no longer treats inventory devices as changed: a stored hostname that resolves to the discovered IP is a no-op, a hostname is never replaced by an IP, and an address change of a device with a password is a suggestion ("Known device - address changed" with "Update address" for admins, or "Dismiss"), not an update. The scheduled scan wrote 255 refused device.upsert audit rows; no-ops now write none and a refused attempt is audited at most once per device and day (in memory, so a restart may add one).
  • Credentials mean a non-empty password. A web user without a password (all TasmoAdmin imports) is loaded as "no credentials", dropped on TasmoAdmin import and when a device is saved; the stored http_user values are not deleted by the migration. Such devices follow DHCP changes again and no longer block moves.
  • The Discovered list never lists a device whose MAC is in the inventory as new.

0.6.1

Fixed
  • Tooltips: hovering a page no longer copies the whole page text into a tooltip (only cut-off table cells get their full text), the browser no longer shows a second, native bubble next to the tooltip, and every tooltip is capped at 160 characters. The resize handle is gone from the last column and from trailing pinned columns.

0.6.0

Added
  • Devices: separate Uptime and Wi-Fi columns (compact "12h 55m"; signal bars with the percentage, dBm on the tooltip) and a hidden-by-default Last restart column, all in the Columns menu. The Status cell shows MQTT and HTTP per channel ("MQTT ✓ / HTTP –") with the last contact of each on the tooltip (the health tracker now keeps the last MQTT and HTTP contact separately; the health state itself is unchanged).
  • Sortable columns in the shared table component: a header click sorts ascending, the next descending (arrow, aria-sort). Server-side through ?sort=&dir= (works without script, keeps filters and paging), typed keys (numbers, durations, times, semver-aware firmware versions, IP addresses, status by severity), the last sort is saved per user and table. On the Devices and Audit tables.
  • Columns: double-click (or Enter on) a resize handle fits the column to its widest content, capped at 40 characters; saved like a manual resize.
  • Columns: reorder by dragging a header or with the move buttons of the Columns menu; the order is saved per user and rendered by the server; the selection column stays first and Power and row actions stay last. "Reset to defaults" restores the order.
Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • Device settings: the "flash wears out" confirmation and the "Write to flash now" box are gone. Applying always ends with exactly one SaveData write (so it works with SaveData 0), skipped only when a restart or a SaveData change saves anyway; the page says "Saved to the device once". The old confirmation decision is marked superseded in the security docs.
  • Header tooltips of every table are one short sentence saying what the column shows; the other tooltips are shortened. Tooltips have their own layer: at most 32 characters wide, normal line height, contrast of at least 7:1 in the light and the dark theme.
  • Relative times are compact everywhere ("now", "45s", "5m", "2h", "3d"), the full timestamp is on the tooltip; the "seen" prefix is gone where the column says it.
  • The Columns button sits in the table toolbar (right end, same height as its buttons), or in a bar of its own above tables without a toolbar.
  • The Devices "Vitals" column is split into Uptime and Wi-Fi (saved layouts of the old column fall back to the defaults for it).
Fixed
  • Phone: on the Devices list the power toggle and the selection box lay partly outside their row (a negative margin grew the 40 px target beyond the row). They are centred on the whole two-line row now and always inside it; no row action uses a negative margin any more.

0.5.0

Added
  • Table columns on every data table: resize by dragging the header edge or with the keyboard (arrow keys, Home resets a column), show or hide columns in a Columns menu, Reset to defaults. Saved per user on the server (migration 0012 user_prefs, endpoint POST /prefs/{key}), so they follow the user across browsers and sign-in modes; the layout is rendered into the HTML, so the page does not jump. The page size of paged tables (25/50/100) is saved with them. Desktop layouts only; phones keep the dense list.
  • Tooltips: status badges, family and scope chips, relay buttons, update options and counters explain themselves (the Stale hint names the configured threshold); a value a cell cuts off shows its full text on hover or focus; every system setting has a help text.
Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • UX sweep: one vocabulary ("Transaction", "prefetch", "Users"; audit targets read "transaction #3" and device names, as links); plurals agree ("1 deviation"); setting labels no longer show raw backticks; durations read "6h", not "6h0m0s". Phones: every button, summary, link in a row, toggle and checkbox is a 40 px touch target. Users and Change password are part of the app (navigation, sign-out); only sign-in and the forced first password change stay centred. Device page: Console, Settings and Edit in the header, Delete in its own section. Device settings: jump links, changed fields marked, sticky Apply bar. Firmware releases: the changelog is the release link, "Prefetch for my fleet" shows only where files are missing. Policy: devices first (those to fix on top, unchecked ones folded), rules below. Rules: "Add rule" opens the editor above the table with the name focused. Update: sticky "N selected / Preview" bar, shorter policy options, the variant confirmation only with a variant. Transaction page: target, starter, start and duration; only non-zero counters. Setup: the networks banner is a warning and no longer links to the setup from the setup.
  • Phone: tabs (add devices) and the settings sections are one select instead of wrapped chips (the links stay without script); a transaction card opens as a whole; cached firmware files show a short hash with a copy button; sizes ("6.5 KiB") never wrap.
  • The streamed tables (prefetch files, scan results) use the saved column layout too.
Fixed
  • The audit target filter treats batch:N and transaction:N as one subject, so the history from before the rename is found (the rows themselves are not rewritten).
  • Setup uses the same device-networks field as System settings; the users table and the new-user card have one width; a device's state-change list no longer repeats the source.
  • Bulk "Switch on/off selected" were clickable with nothing selected; they stay disabled until a device is selected. A failed local sign-in keeps the username and puts the cursor in the password field. A backup whose snapshot is incomplete shows a warning badge. Deleting a local user also deletes its interface preferences (user_prefs).

0.4.0

Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • One sign-in page: /login shows a "Sign in with <name>" button (OIDC) and the local username/password form side by side. The browser goes straight to the IdP only when OIDC is the only method (local sign-in off or no local account). The button name is the new sign-in setting OIDC_DISPLAY_NAME (default "Single sign-on"; any provider). GET /login/sso starts the OIDC flow; GET /login/local redirects to /login. In header mode, visitors the proxy did not identify get the local form too.
  • Phone density (below 48rem): tables become compact list rows of at most two lines with one actions menu per row instead of stacked label/value cards, on devices, device detail, firmware, policy, the rules editor, transactions, the update picker, audit, the system settings and about pages. Long sections are folded by default (cached files, state changes, update history, backups, rule notes, the scope groups of the rule editor, per-device findings, import, setup steps 2 to 4, filters, more update options); the audit log shows 25 events with a "show more" toggle. Primary actions sit in a bar fixed to the bottom of the screen (device, rule editor, update preview, setup). With the realistic seed every page's main view is about two screens or less (was up to 15). Wide screens are unchanged except as listed under Fixed.
  • One pager for paged tables (the audit log now): first, previous, a window of page numbers, next, last, "Page X of Y, N events" and a page size of 25, 50 or 100 (per parameter, default 25) on wide screens; previous, "X / Y", next on phones. Plain links, filters stay in the URL.
  • Audit log: the Details column is a small button; hovering or focusing it shows the JSON as a tooltip, clicking expands the full row (the whole Result text and the JSON). Result is the wide column and wraps (three lines at most, full text in the title).
  • Devices table (1280 px and wider): Name is a fixed column of about 22 characters (longer names are cut, the full name is in the title), Status, Family and Firmware are compact, Address is cut with a title, Vitals takes the rest and does not wrap, Power has a fixed width.
  • /policy/rules uses two columns only from 88rem; narrower, the rules and versions tables stack (side by side they were wider than a 1280 px window).
Fixed
  • /policy/rules was 668 px wide on a phone and 1807 px wide on a desktop: the versions table showed a full 40-character identifier (the user of a version, or one quoted in its note) that cannot wrap. Such tokens now show their first 8 characters with the full value on hover.
  • The navigation row was 5 to 15 px wider than the screen on every phone page (the last group's menu button ended at 381 px of 375). It uses less padding and wraps instead of overflowing.
  • An opened audit entry could not be closed again: its only child was hidden, so the summary had no height. The summary now always shows "Details".
  • Fixed action bars on phones no longer cover the last control of the page (bottom padding).
  • /devices between 768 and 1152 px was up to 280 px too wide (Power column): the Family and Vitals columns give way and cells wrap there.
  • The overflow e2e check never saw any of this: it compared scrollWidth with innerWidth, which includes the scrollbar (html reserves its gutter), so 9 to 15 px of real overflow passed; it measured before the fonts loaded, had no 40-character identifiers in its data, and never looked inside the navigation row. It now measures against the visible width with zero tolerance after document.fonts.ready, seeds policy versions with real 40-character hashes, opens the navigation menus and fails when the navigation row scrolls inside itself. A new e2e spec asserts every page's height budget on the realistic seed.
Breaking
  • breaking: the direct admin listener is removed (decision 2026-10-19): ADMIN_LISTEN, ADMIN_NETWORKS, ADMIN_TLS_CERT_FILE, ADMIN_TLS_KEY_FILE and ADMIN_TLS_SANS no longer exist, nor does port 8443 of the Unraid template, the tasmofleet tls fingerprint command, the __Host-tasmofleet-admin* cookies and the audit via: admin-listener tag. A variable that is still set is ignored with one warning per key at start (no crash, data untouched). Break-glass is a local admin on the normal login page plus tasmofleet user passwd; the only protection is the existing login rate limiting, which no longer has an ADMIN_NETWORKS exemption. Remove the variables and any firewall rule for 8443. Sessions of the removed listener are deleted by a migration; <DATA_DIR>/tls/admin/ (its certificate) can be deleted. See docs/runbooks/emergency-access.md.

0.3.2

Fixed
  • With a stored MQTT broker, /, /devices and /about could hang forever after start (v0.3.1): the health service asked mqttbus.Switch.Enabled() under its own lock while Switch.Replace held the switch lock across the new connection's retained-message replay into health. Enabled is now lock-free and the switch no longer holds a lock while calling the connection or handlers.

0.3.1

Fixed
  • After a restart, MQTT devices showed Online but without uptime, Wi-Fi and power until their next tele/STATE (up to one TelePeriod): a retained LWT made them look healthy, so no poll ran. A device that is alive but has no state snapshot now gets one Status 11 fetch at once (paced to the poll concurrency per tick, retries spaced by a minute; never Status 0).
  • Phone layout: /policy (the "Applies to" chips did not wrap), /firmware (device link lists in the cached-files table made the page twice as wide) and /policy/rules no longer scroll sideways. A scope that covers a whole chip family reads "ESP32 (all variants)" / "ESP8266" on the policy page and in the rules editor. On phones the release list shows the newest three releases with one actions menu per row and a "Show older releases" toggle; desktop is unchanged. The policy page has a prominent button to the rules editor.
  • The navigation dropdown of the last group opens to the left, so it cannot leave the screen; the transactions table scrolls inside its card when it is wider than the space.
  • The overflow e2e checks now run on realistic data (27 devices, 10 releases, cached files, the ESP32 exception rule, audit entries, a retried transaction) and report the widest elements.
  • The one-time legacy import now also imports TASMOFLEET_MQTT_USER and TASMOFLEET_MQTT_PASSWORD when they are separate variables (they were dropped, so MQTT connected without credentials); the password stays sealed and bound to the broker. tasmofleet settings import help now says that a running server applies the file only after a restart.

0.3.0

Added
  • First-run setup: device networks, MQTT, SSO pointer and a scan; a banner while no device networks are set.
Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • Settings apply live: device networks, MQTT broker, backup retention, update engine and health intervals need no restart.
  • The live-check poll interval is the system setting POLICY_LIVE_INTERVAL (default 1h, 0 = off).
  • Step-up on the System settings page is limited to the sign-in settings; routine settings (device networks, MQTT, firmware, retention) no longer ask (proportionate security, docs/security/system-settings.md).
Fixed
  • The one-time import of legacy environment settings is performed by serve only; CLI commands (settings, user, ...) no longer set the import marker, so a CLI run before the first start (possibly with another environment) cannot silently drop the legacy settings.
Breaking
  • breaking: operational environment variables are removed: DEVICE_NETWORKS, FIRMWARE_BASE_URL, FIRMWARE_SOURCES, FIRMWARE_ALLOW_SINGLE_SOURCE, FIRMWARE_REFRESH_INTERVAL, FIRMWARE_KEEP_RELEASES (was unused), OTA_*, BACKUP_*, POLL_INTERVAL, TELEPERIOD, MQTT_*, DISCOVERY_*, METRICS_DEVICE_LABELS, AUDIT_RETENTION, POLICY_RULES. They are settings of the System settings page now. On the first start of this version a variable that is still set is imported once into the database (only if the database has no value for the key, with a warning per key; credentials in MQTT_URL move to the sealed password; POLICY_RULES becomes the first managed rule version) and ignored afterwards, so remove them from the container configuration. tasmofleet settings export|import <file> moves settings and policy rules as JSON.
  • breaking: AUTH_MODE defaults to local, and without TRUSTED_PROXIES, certificate or ALLOW_INSECURE_HTTP the server serves a self-signed certificate (LISTEN defaults to :8443 then). A fresh container needs no configuration. The sign-in settings (AUTH_MODE, OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_SCOPES, OIDC_*_CLAIM, ADMIN_GROUPS, OPERATOR_GROUPS) can be edited on the System settings page (step-up, "test SSO"); the environment still wins and then locks them.
  • Without DEVICE_NETWORKS the server starts and contacts no device.
  • Policy rule editor rebuilt: the rules are a table (setting with its label, expected value, scope chips, severity, live findings count) with an editor panel for one rule at a time and the versions beside it. The expected value uses the control of the setting (dropdown, on/off, number with range, text), the scope uses chips from the known chip families and the inventory's tags and devices, and a live preview says how many devices the draft would flag. breaking: POST /policy/rules (save the whole list) is replaced by POST /policy/rules/save and /delete for one rule; importing JSON now compares with the saved rules and needs a confirmation step instead of loading into the editor.
Security
  • MQTT broker URLs with ws:// or wss:// are refused (live connection, connection test and settings validation): paho dials them outside the address filter, so they bypassed the always-blocked ranges. Use tcp:// or ssl://.

0.2.0

Added
  • Policy checks read live device values first (side-effect-free queries, hourly jittered poll, MQTT stat updates); backups are only a labelled fallback.
  • Devices: bulk switch on/off for selected devices (confirmation for more than one, per-device results).
  • Audit log with filters, paging and expandable details; firmware release dialogs; guided update page; system settings in sections.
Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • Navigation grouped (Devices, Updates, System menus); one wide page container; stable device table and relay switches; static assets are cache-busted.
  • Raw HTML in release notes is dropped instead of being shown as text.

0.1.0

From this release on tasmofleet follows SemVer (see CLAUDE.md, "Releases").

Added
  • System settings page (/settings/system, admin): operational settings stored in SQLite (migration 0010), environment values lock their field, bootstrap/security settings stay environment-only, many settings apply live (docs/security/system-settings.md). New keys DISCOVERY_MQTT, DISCOVERY_SCAN, DISCOVERY_SCAN_INTERVAL, DISCOVERY_SCAN_CIDRS.
  • Policy rule editor (/policy/rules): structured rules with name, severity, tag/device scope, preview, versions, diff, revert, JSON import/export.
  • Firmware page: release changelog, fleet coverage per release, recommended release, prefetch for the fleet, delete one or all unused cached files.
  • Transaction retry: retry all failed/cancelled devices of an update or a selection; the new transaction links to the original (migration 0009).
  • UI design pass: design system, status badges with text and glyph, stacked tables on phones, bulk selection, calm transaction page with step bar, /transactions list.
Changed
  • The bundled decode-config is the upstream Ubuntu 24.04 build (newer Python runtime); the runtime image already had the required glibc
  • /jobs is replaced by /transactions and /transactions/{id}.
  • The policy view and its navigation entry always exist (empty state without rules).
Fixed
  • Power toggles and console commands could run during restore_power; it now counts as an active OTA state for the device lock.
  • A retry restores the device's original OtaUrl, not the tasmofleet URL left by the failed attempt.
  • Direct ESP8266 plans record the minimal artifact they cache.
Breaking
  • Editing a device so that its address or port changes while the stored device password stays is refused: type the password again or remove it. Changing the MQTT broker needs its password again; an environment MQTT_PASSWORD/MQTT_URL locks both.
  • DEVICE_NETWORKS may now come from the stored settings; the start still fails if the effective configuration has none. More keys are validated (FIRMWARE_SOURCES, MQTT_URL, POLL_INTERVAL/TELEPERIOD >= 1s, AUDIT_RETENTION >= 24h, BACKUP_INTERVAL 1s..1m).
Security
  • Stored MQTT password is bound to its broker address and never sent to another one.
  • Release-note links only to an allow-list of hosts; hostile Markdown is rendered as text.

0.0.6

  • breaking: CLI tasmofleet admin set-password|delete removed. Use tasmofleet user passwd --create --role admin <name> (also promotes an existing user for recovery) and tasmofleet user delete <name>.
  • OTA power restore after an update: lights by default, relays opt-in per batch, never switches anything OFF, fails closed (migrations 0007, 0008; OTA_POWER_MAX_AGE).
  • MQTT broker classification per device (migration 0006).
  • mqttbus: one canonical device key; discovery topics decode only with the canonical upper-case MAC (docs/security/mqtt-ingest.md Q11/Q12).
  • Address display unified (internal/netaddr).

0.0.5

  • Discovery: MQTT candidates, HTTP autoscan, confirm new address (migration 0004).
  • Console and power toggle; command policy is an allowlist.
  • Backup diff and one-time-token restore (migration 0005).
  • One device lock across restore, OTA, settings and console/power.
  • Hardened MQTT ingest; audit via tag for the admin listener.

0.0.4

  • Direct HTTPS admin listener for emergency access (ADMIN_LISTEN, ADMIN_NETWORKS; migration 0003).
  • Firmware index refresh at start and periodically (FIRMWARE_REFRESH_INTERVAL).
  • Branding, settings editor and policy view, TasmoAdmin web import, OIDC step-up.

0.0.3

  • Firmware index parses the real ota.tasmota.com listing and GitHub index (OTA in 0.0.2 found no files); factory images refused; minimal image always cached.
  • OTA and backup integration suites; MQTT upgrade hints; batch cancel.

0.0.2

  • OTA UI, standalone mode (AUTH_MODE=local, local accounts, self-signed TLS), OIDC group re-check (migration 0002 ends existing sessions).

0.0.1

  • First walking skeleton: inventory, health, backups, OIDC/header/local login.