Skip to content

Releases: Higangssh/homebutler

v1.2.0

Choose a tag to compare

@github-actions github-actions released this 12 Oct 00:48
4474b26

Every alert watch sent was dated the year 1, and a container stopped on purpose had no way to stay out of them.

Every container and threshold alert from watch start reached its channel stamped 0001-01-01 00:00:00, on every provider. It turned up while checking #338 against a real webhook on the Raspberry Pi. alerts.ignore comes from that report, by @2lach: containers a cron job stops and starts every night paged them on each stop and each recovery. The watcher already reported each change once rather than every check, so the stops were real, and what was missing was a way to say they were intended. install purge now also removes the volumes of an app whose container had been recreated, which 1.1.0's purge fix still left behind. config validate no longer reports a working alerts.rules block as an unknown key with no effect.

⚠️ Behavior changes

  • install purge removes the anonymous volumes of an updated app (#335, #336). Before this release they survived the purge whenever Compose had recreated the container, which happens on every docker compose up after an image update. Data that a purge used to leave behind by mistake is now deleted, as purge says it is. 1.1.0's purge fix (#326) had only been checked against freshly installed containers. Its own report gave the gap away: purging the README demo's filebrowser printed Removed, except: kept volume … (still exists) twice rather than Completely removed. Purge now also removes the volumes this app's containers used that Docker labels anonymous and that no container still uses. A named, external or in-use volume is still left, and listed

✨ Features

  • alerts.ignore names containers whose stopping and starting is intended, such as one a cron job stops every night (#338). watch start and alerts --watch report neither their going down nor their recovery. Nothing else changes: every other container is still reported, once per stop and once per recovery, as before. config validate lists the ignored names, and saving a threshold from the dashboard keeps the list

πŸ› Fixes

  • every container and threshold alert from watch start reached its channel stamped 0001-01-01 00:00:00 (#341). watch passed the time as RFC 3339, the conversion only parsed the rules engine's layout, and the error was dropped. Both forms are read now. Found while checking #338 against a real webhook
  • config validate warned that alerts.rules was an unknown key with no effect, while watch was acting on those rules (#338). The validator checked against a schema that has no rules field, and watch reads them through a different struct. It now checks each rule against the type watch decodes into: a misspelt key inside a rule is reported at its line, and a rule watch would refuse is reported as an error. A test fails if watch reads a key that validate does not accept

v1.1.1

Choose a tag to compare

@github-actions github-actions released this 11 Oct 15:02
714299a

An idle Linux machine read CPU 0.0% because it was measured over a fifth of a second, which on four cores cannot show anything between 0% and about 1.2%.

The README's new recording showed it first: the Raspberry Pi it was recorded on averages 0.65% over half a minute, and status read 0 five times running.

πŸ› Fixes

  • CPU usage on Linux is measured over one second instead of 200 ms (#332). A Raspberry Pi averaging 0.65% read 0.0% every time: 200 ms on four cores is about 80 of the kernel's ticks, so the reading could only be 0% or about 1.2%, printed to two decimal places. A second gives steps of 0.25%, the same span macOS already samples over. An idle machine can still read 0.0% for a second that really was idle. status, the dashboard and the alerts watcher each wait 0.8 s longer for the number. status --all and the dashboard's server overview ask every server at once, so that is 0.8 s per call, not per server

v1.1.0

Choose a tag to compare

@github-actions github-actions released this 11 Oct 12:34
0b913fe

Recreating a container is now reported once, as the container, instead of again as the processes that ran inside it β€” and install purge removes the data it says it removed.

Both were found while recording the README's demo with 1.0.0. On a day when two containers were recreated and one of them changed image, report printed seven lines. Five of them repeated those two changes through the process table: each container's containerd-shim with its new ID, the process inside uptime-kuma that its new image renamed, and the image's entrypoint with a different command line. This release prints the two replaced lines. The first report after upgrading compares against a snapshot 1.0 wrote, which still holds every container's processes. If a container was recreated between that snapshot and the upgrade, its old processes are reported gone that one time. Clearing up the demo machine then turned up the second problem. install purge filebrowser had said Completely removed and left filebrowser's database and settings in two Docker volumes, there and on the soak machine before it.

⚠️ Behavior changes

  • report no longer compares the processes that belong to a container or to watch (#315). Recreating one container used to produce seven lines where two said anything. Each container's containerd-shim carries the container's ID, so every recreation read as a new process, and the processes inside the container came and went with it. The container line already says the container was recreated, with its image. A process is placed by walking its parents, which ps gives on Linux, macOS and busybox alike. The docker events helper watch start runs is left out too, so an upgrade that changes its arguments is no longer replaced: docker. The watch process itself is still compared. What report gives up is a command line changing inside a container that kept running. The first report after upgrading compares against a snapshot that still holds every container's process, and those are matched and left out of it as well, except for a container recreated between the two runs, whose old processes are reported gone that once
  • install purge now removes the Docker volumes the app's containers used, and names them (#324). It said βœ… Completely removed (containers + data) and left filebrowser's database and settings behind. The image declares /config and /database as volumes that the compose file does not map, so Docker gave each container an anonymous volume, and compose down keeps those. Purge now runs down -v and lists each volume it removed. It says Completely removed only when Docker confirms every one is gone. A volume that still exists (external, or used by another container) is listed as kept under Removed, except:, and one Docker could not be asked about is listed as unchecked, never as removed. No catalogue app declares a named or external volume, so what it removes beyond the app directory is exactly these. install_purge over MCP and POST /api/install/{app}/purge already answered data_preserved: false, and now that is true. uninstall is unchanged and keeps them

v1.0.0

Choose a tag to compare

@github-actions github-actions released this 11 Oct 06:14
08e764e

From this release, what homebutler answers with is a promise: a tool, a field, a word an agent branches on or a route a script calls does not change before 2.0, and the build fails when something tries. docs/compatibility.md lists what is frozen and what may still be added; go test ./internal/contract is the check that holds it.

Before the tag, homebutler ran on a Raspberry Pi with three apps, a watch service and a daily report --json for eleven days, from 2026-09-30 to 2026-10-11 β€” starting on 0.40.0, with each fix going back onto the same machine as 0.41.0 through 0.41.3. Most of what changed in between came from what that server showed: a docker kill filed as an OOM and a restart recorded that never happened (0.41.0), a quiet day reported as a comparison that could not be made and lists that came back null (0.41.2), and a changed image reported as a port taken over (0.41.3). The last three reports, from 0.41.3, named the image change made on purpose and a system service that had exited, then two quiet days with every list empty. Three of the 0.41 fixes changed what an answer looks like: incident start times, the keys of two answers, and lists that were null. They went out before the freeze for that reason. The days on 0.41.3 needed no change at all. Apart from the release workflow below, 1.0.0 is 0.41.3's code.

♻️ Internal

  • a release checks its npm and Homebrew credentials before it builds anything (#317), and a Credentials workflow runs the same check from the Actions tab. v0.41.3 found at the last step that npm refused its token, with the binaries, the image and the tap already published, and a tag cannot be published twice. Running the Credentials workflow after replacing a secret shows whether the new one works without cutting a tag. The Credentials workflow also runs every Monday, so a token that expires fails a run that week instead of the next release (#320)

v0.41.3

Choose a tag to compare

@github-actions github-actions released this 08 Oct 06:17
24c6b83

The soak's day with a deliberate image change reported the change correctly and then also told the operator that a port had been taken over by the container that had held it all along. It was the one false line left in the 0.41.2 soak's reports, and this release removes it along with its duplicate. It also stops upgrade from replacing a build that is newer than the latest release with that release β€” the build from make included, which is stamped by git describe β€” in a fix contributed by @mvanhorn. And it is the first release built by release.yml with the v4 Docker actions, so that 1.0 is not their first run.

πŸ› Fixes

  • upgrade replaced a binary that was already newer than the latest GitHub release (#307, #312). It treated any version string that was not exactly that release as out of date, so a source build stamped 0.41.1-dev was moved to v0.41.0, and a machine already on a newer tag was moved back while the latest release was still the previous one. A higher MAJOR.MINOR.PATCH is left in place, including one with a suffix, and the line for that machine says it is newer; the summary still counts it as up to date. A make build is stamped by git describe, and v0.41.2-2-gc180322 or v0.41.2-dirty counts as after its tag, so it is left alone too. A real prerelease with the same numbers, such as -rc.1, is still upgraded once that release is out, and a local dev build with no numbers is still refused
  • upgrade printed two vs in front of a version that already had one (#313): already vv0.41.2 for every make build exactly at a tag, since git describe stamps the v itself
  • report said a port had a new owner when only its container's image had changed (#314). A Docker container owns a port under the label name (image), and the comparison used the whole label, so a container whose image tag moved β€” or one recreated on a new image β€” was reported as port: :8082/tcp β€” it-tools (corentinth/it-tools:latest) β†’ it-tools (8b8128748339), with a needs_attention item saying the port "was not" answered by it at the last report. The image change is already the image line, and a recreated container is replaced. A port now changes hands only when the process or the container's name does. Found on the 0.41.2 soak, the day after an image tag was moved deliberately
  • a port reachable on both 0.0.0.0 and [::] that changed hands produced its needs_attention item and its suggested action twice (#314), once per listener. notable_changes already counted it once

πŸ“¦ Distribution

  • the release workflow uses docker/setup-buildx-action and docker/login-action v4 (#301, #302). They were held back because release.yml runs only on a tag, so CI never exercises them; this release is their first run, ahead of 1.0

v0.41.2

Choose a tag to compare

@github-actions github-actions released this 03 Oct 04:24
cf4d903

On the first quiet day of the soak, report --json said the comparison could not be made, because "no significant changes" was filed under the kind that means exactly that. This release makes an empty answer look empty: a comparison that found nothing returns [], no list anywhere comes back as null, and the two answers that used Go's field names as JSON keys use snake_case like every other. All of it changes what a caller receives, which is why it ships before 1.0 rather than after: 1.0 freezes these shapes, and they would have been frozen saying the wrong thing. A test now fails on a capitalised key, and another on a list that would reach a caller as null, so neither can come back.

⚠️ Behavior changes

  • A report with nothing to say now has an empty notable_changes (#308). It used to carry one skipped line, which by its own definition meant the comparison could not be made: "No significant changes since last report" was filed under the kind for "could not compare". The terminal still prints that sentence; --json and the MCP tool now return []. Found on the 0.40.0 soak, whose first quiet day said skipped. A first run and an unreadable previous snapshot still carry their skipped line, because for them the comparison really was not made
  • needs_attention and suggested_actions are [] when there is nothing in them, not null (#308), and so is notable_changes. null says the field is missing; [] says there is nothing in it. A caller that checked == null for "nothing to do" now has to check for an empty list
  • install_list, GET /api/install and a watch incident's flapping use snake_case keys (#309). They were the only answers with capitalised keys: Name, DefaultPort and the rest for an app, IsFlapping, Count and the rest for flapping, because neither struct had JSON tags. install_list also no longer carries each app's whole compose template. A caller reading the old keys breaks; it breaks now because 1.0 freezes key names, and these would otherwise have been frozen as Go's. Incidents already on disk with the old flapping keys are still read
  • No list in any answer is null (#310). alerts_history on a machine with no history answered null over MCP while --json printed [] for the same thing, and a list nested in an answer β€” system.disks when no disk could be read β€” could be null wherever nothing had been appended to it. MCP, the HTTP routes and --json now pass every answer through one function that writes an empty list or map where null would have been, at any depth. A field with omitempty and a nil pointer still mean "absent" and are left alone

πŸ› Fixes

  • the dashboard's report card showed a bare skipped chip on a quiet day (#308). It now says No significant changes since last report. when the list is empty

v0.41.1

Choose a tag to compare

@github-actions github-actions released this 03 Oct 02:24
938388c

deploy had not worked since 0.10.0 for any account that cannot write /usr/local/bin, and the only thing it said was Process exited with status 1. A shell-injection fix quoted the fallback install directory, $HOME/.local/bin, so that the remote shell never expanded it; the passwordless-sudo account failed separately, and in both cases scp's own reason was lost because the client never read its answers. Reported in #303 by @wardbryan3. Each fix here was checked against two containers, a plain account and one with passwordless sudo: 0.41.0 fails in both, 0.41.1 installs and runs in both.

⚠️ Behavior changes

  • A server's bin that starts with ~ or $HOME now takes effect (#304). It was quoted whole, so the remote shell looked for a file literally named ~/…, and every remote command for that server failed. A bin like ~/.local/bin/homebutler now reaches the binary it names
  • A server's bin that uses any other variable is refused (#304), with an error naming it, rather than quoted into a path that cannot exist. Only a leading ~ or $HOME is expanded

πŸ› Fixes

  • deploy failed on every account that could not write /usr/local/bin, and said only Process exited with status 1 (#303, #304). Since 0.10.0 quoted remote paths against shell injection, the fallback $HOME/.local/bin reached scp quoted and unexpanded, and scp was told to write into a directory named $HOME. The directory is now read from the remote as an absolute path before anything is quoted, and a test in internal/remote fails when a path is quoted without going through the helper that leaves a leading ~ or $HOME to the remote shell. The account with passwordless sudo failed too: deploy chose /usr/local/bin for it and then uploaded there without sudo. It now uploads to a temporary file and moves it in with sudo -n install. Reported by @wardbryan3, with a reproduction
  • an upload scp refused never said why (#304). The client sent the whole binary without reading scp's answers, so a refusal arrived while twelve megabytes were still being written and the channel closed with the message unread. It now waits for each answer, and deploy and upgrade report the line scp gave: scp: /home/bob/.local/bin/homebutler: Permission denied
  • deploy checked the install by running whichever homebutler came first on PATH (#304), which on a machine that already had one was not necessarily the binary it had just written. It runs that binary by its path, and a shell rc file that already has the old $HOME/.local/bin line is not given a second one
  • upgrade stopped at Permission denied on a remote binary it could not write, without saying what would (#305). Since #304 deploy installs through sudo where the account has it, so the binary can be root's, and upgrade writes as the login user. It now names the path and the user and points to homebutler deploy --server <name>, which replaces it through sudo β€” checked in a container, where it took a root-owned 0.40.0 to 0.41.0

♻️ Internal

  • the release waits up to thirty minutes for npm to serve a version before publishing it to the MCP Registry, polling npm view (#306). 0.41.0 took about sixteen minutes to appear on npm after a publish npm had accepted, and the five-minute wait failed the release at its last step with every other channel already out, so the registry still lists 0.40.0. This release puts it back in step

v0.41.0

Choose a tag to compare

@github-actions github-actions released this 30 Sep 13:56
1e7fb0b

The first day of running 0.40.0 on a real server found homebutler stating things nobody had told it: a docker kill filed as an OOM with high confidence, a restart recorded that never happened, and a command suggested that does not exist. This release fixes those and adds nothing new β€” every change corrects something homebutler said without being told, because it is the build that runs on that server before 1.0, and 1.0 will be what it does. Each fix makes homebutler say only what the machine reported: oom only when Docker sent an oom event, a start time only when something started, and in one format whichever of Docker, systemd and pm2 it came from.

⚠️ Behavior changes

  • A watch incident that ended in exit 137 is oom only when the OOM killer was involved (#295). Every 137 used to be oom with high confidence, so docker kill, a docker stop that ran out of time and kill -9 were all filed as OOM. The Docker monitor subscribed to die alone and never received the oom event Docker sends just before an OOM death. It now subscribes to oom too: a die that follows its container's oom within ten seconds is OOM-killed, with docker inspect as a second source, and a 137 without that evidence is unknown, low, signal SIGKILL. An agent that branched on oom now receives it only for an OOM. Found on the 0.40.0 soak: a docker kill came back as oom/high while docker inspect said OOMKilled=false
  • An incident's prev_started_at and curr_started_at are RFC 3339 in UTC or empty, from every monitor (#295, #297). A script that parsed the old values breaks, because each monitor wrote them in the form it received. Docker's said died at event time 1790772824 and (post-restart), the second whether or not anything had restarted; systemd's were its local text, Wed 2026-09-30 21:46:44 KST; pm2's were sentences, restart_time was 0. prev_started_at is now the start of the run that died and curr_started_at the start of the run after it, or empty when there is none. systemd is asked for --timestamp=unix, and on a systemd older than 248 the local text is read only when its zone is the machine's or UTC, because Go reads an unknown abbreviation as UTC and the time would come out nine hours wrong. pm2's are pm_uptime, checked against a real pm2 around a restart; the restart count was already in restart_count

πŸ› Fixes

  • install finished by suggesting homebutler logs <app>, a command that does not exist (#296). It is homebutler docker logs, with the container's name rather than the app's, since the two differ for pi-hole. The same kind of hint sent people to a missing homebutler tui in #159, so a test now finds every string that tells someone to run homebutler and resolves it against the command tree. Found on the 0.40.0 soak, which ran the hint and got unknown command "logs"
  • watch install and serve install told you to enable lingering when it was already on, and to use sudo for it (#296). They now ask loginctl first and say nothing when lingering is on. When it is off, the command they give is loginctl enable-linger without sudo, which systemd allows for your own account by default, with sudo as the fallback
  • watch list showed LAST CHECKED frozen at the moment a target was added while the watch service was watching it all along (#297). The column is the last watch add or watch check, and with the service running neither happens. The list now says when the service is live and since when, and what the column means. The JSON is unchanged

πŸ§ͺ Tests

  • crash_analysis.category and confidence are in the golden file (#295). They are what an agent branches on after an incident, and they were outside the freeze, which is how a wrong oom could have been frozen

v0.40.0

Choose a tag to compare

@github-actions github-actions released this 27 Sep 23:49
49be447

1.0 promises that what homebutler answers with will not move under the people who build on it, and until this release most of that promise could not be checked. The golden file read the fields of three commands and named the type of everything else, so a field renamed inside docker_list, a status word dropped from install_purge, or a snapshot an older release wrote could all change with the build still green. All of it is under the test now: every tool's answer field by field, the words a status or an argument can hold, the five Proxmox reads, and a snapshot from each release whose format changed.

Settling that turned up what would have been frozen wrong. Six numeric arguments were declared as strings or as fractional numbers, schema_version could not have been added after the freeze, and the Proxmox shape the guest actions read their targets from was recorded as somebody else's. It also turned up three places where homebutler reported something nobody had told it: report called an unreadable history a first inspection, proxmox_node gave a node zero cores when Proxmox had not said, and proxmox_status had never once shown the repository id Proxmox sends.

homebutler backup --exclude /mnt/media     # leave a bind mount out of the archive, and say so in it

⚠️ Behavior changes

  • Six numeric arguments are declared as integers instead of strings or fractional numbers (#288). docker_logs lines, processes limit, watch_history limit, install_app port, report keep and doctor backup_max_age_hours. A call that sends "50" still works exactly as before, and a test holds both spellings; what changed is what the schema tells an agent to send. keep and backup_max_age_hours were number, and a fractional value sent to either was already cut to a whole one; the schema now says so. 1.0 freezes the schema, and an argument left as a string would have stayed one
  • The Proxmox guest actions declare type as qemu or lxc (#288). Those were already the only two values accepted, so every call that worked before still works; the schema now says so instead of the description
  • report --json, doctor --json and status --json carry schema_version: 1 (#288), as do the matching MCP tools and dashboard routes. After 1.0 a new field cannot be added without a caller guessing whether its absence means an older homebutler, so the version goes in before the freeze. It is on the top-level document only: the system object nested inside report and inventory_scan does not carry one, because there it would read as the version of the document around it
  • report warns when it cannot read the previous snapshot (#289). It used to treat an unreadable snapshot exactly like having none: the run became a baseline, and the note said First inspection β€” no previous snapshot found. So a damaged file, or a format change in an upgrade, ended the comparison history with nothing on the screen to show it. It is still a baseline, and the report is not stopped, but warnings now names the file and the reason, and the note says nothing was compared. A first run, with no snapshot at all, says nothing new
  • proxmox_node leaves out a count or flag Proxmox did not send (#291). cpuinfo.cores, cpuinfo.cpus, cpuinfo.user_hz and boot_info.secureboot were written as 0 or false when Proxmox left them out, which told an agent a node had no cores or secure boot off when nobody had said so. They are now left out in that case, and written as before when Proxmox sends them, 0 and false included

✨ Features

  • a bind mount can be the wrong size to archive, and there was no way to say so (#282, #283). A media server mounts a library directory and backup tarred it whole, every time β€” reported from a real Jellyfin setup where that directory is terabytes. --exclude leaves a path out, covering anything mounted under it on path boundaries, so /mnt/media covers /mnt/media/movies and not /mnt/media-backup. It applies to bind mounts only; a named volume is Docker's own directory and has no host path to match. What was excluded is written into the archive, and the mount stays in the manifest marked excluded rather than being dropped from it: somebody restoring months later has to be able to tell this service had a media directory and it is not in here from this service had no media directory. A path that matched nothing is reported, because a misspelled --exclude archives the terabytes it was meant to avoid and the archive size is otherwise the only place that shows. The cost was never only disk β€” backup drill boots an archive and requires it to answer a health check, and a terabyte archive cannot be drilled, so the data nobody wanted backed up was what stopped the verification from being usable on the data they did

πŸ› Fixes

  • the demo dashboard answered a Proxmox guest action with vmid as a string (#288). The real route sends a number. The guest action result had been written out four times, once each for the MCP tool, the CLI, the route and the demo, and only the demo's copy had drifted. There is one proxmox.GuestActionResult now, and all four use it

  • proxmox_status never showed the repository id Proxmox reports (#291). Proxmox sends it as repoid, and the struct only knew the name it is written under, repo_id, so the value was dropped on the way in. The key was always declared and never appeared

πŸ§ͺ Tests

  • every tool's answer is in the golden file, not only the three commands that print JSON (#286). The outputs section named each tool's type and walked the fields of report, doctor and status alone, so a field renamed inside docker_list or watch_list β€” the answers an agent branches on most β€” passed. #283 added three fields to BackupResult and the golden did not move. docs/compatibility.md now freezes every tool's answer

  • the words a status can hold are in the golden file, and so are the words an argument accepts (#287). The golden said a result's status was a string and not which strings, so install_purge could have stopped answering purged, or watch_add could have stopped taking pm2, and nothing failed. The install outcomes had been spelled separately by the MCP tool and the HTTP route. They and four other status sets are now named types with their words declared once. The JSON is unchanged

  • snapshots written by 0.18.0, 0.22.0, 0.26.0 and 0.39.0 are in the repository, and a test compares a report against each one (#289). Each was written by that release's own binary on a real machine, and the snapshot format changed at the first three. A second test fails when a key any of them holds is no longer read, because renaming a key decodes without an error and compares against a zero value

  • the five Proxmox reads are frozen like every other tool (#290). They were recorded as passing Proxmox's own shape through, which was not true: each one decodes into a struct of ours and writes that struct's keys, and resources.guests renames maxcpu to max_cpu and splits tags into a list. The guest actions take their vmid, node and type from that list, so the shape a write depends on was outside the freeze. The values still follow Proxmox; the keys do not

  • a snapshot taken while Docker was down is in the repository, and a test holds that the next report skips its containers rather than calling every running one new (#292). failed_collectors is frozen, and none of the snapshots the releases wrote had ever made a report decode it. This one is written by hand from the v0.22.0 file, and its README says what was changed

v0.39.0

Choose a tag to compare

@github-actions github-actions released this 23 Sep 10:45
8d8c37a

Fourteen things homebutler can do had been waiting on a rule for what a browser must produce before it does them, and while that rule did not exist the dashboard could only look. 0.35.1 gave it a write surface for settings (#154) and decided nothing about running an action, so fourteen capabilities sat in the registry pointing at an issue.

The rule is three tiers, and the question that decides them is whether the thing can be undone by doing something else. It is frozen at 1.0. The other half of the release is that the dashboard can now be running when something needs doing at all β€” serve had no supervised path, so it ran for exactly as long as somebody left a terminal open.

homebutler serve install                 # supervised, on 127.0.0.1:8080
homebutler serve install --port 9090     # somewhere else
homebutler serve installed               # what is installed, and the address it serves
homebutler serve uninstall               # stop it and remove the unit

⚠️ Behavior changes

  • A dashboard token now reaches seventeen writes, and four of them destroy something. Until 0.38.0 the most one could do was send a magic packet, save a snapshot, or send a test notification. The same token now stops containers, restores a backup over live volumes, and deletes an app with its data. Nobody who ran serve --token to edit alert thresholds asked for that, and the token is typed once, lives in localStorage, and belongs to whoever opened the page. Before upgrading, look again at who knows it β€” or install the dashboard on 127.0.0.1, where it answers only the machine it runs on

  • Four capabilities that recorded no view built for it yet now have HTTP routes. backup_list, install_list, install_status and proxmox_guests answer GET /api/backup, /api/install, /api/install/{app} and /api/proxmox/guests. Nothing that worked stops working β€” they are reads, and like every other read they are registered with or without a token. Anything branching on the registry's absence list will see four fewer entries. Three of the four were added because an action beside them could not be called without them; proxmox_guests was not, and docs/compatibility.md says why it was kept anyway

✨ Features

  • the dashboard can run the fourteen actions that were waiting on a rule (#242, #263). They are on three tiers, decided by one question β€” can this be undone by doing something else? Ten run on the click that asks for them: a restarted container comes back, a started guest can be shut down, an extra archive is an extra file. docker_stop and proxmox_guest_shutdown take an explicit confirm, because the service is down until somebody starts it. backup_restore and install_purge take the target's name typed back, because a click cannot say which thing the operator meant to lose and neither comes back. Asking twice for the first ten would buy nothing and teach people to click through confirmations, which is what the other two tiers depend on not happening

  • the tiers are frozen at 1.0 and the golden file records which one each route is (#242). It used to derive protection from the difference between a tokened and an untokened server, which can only say whether a token is needed β€” so all three tiers rendered as token and a capability moving between them was invisible. It reads the declared tier now

  • the dashboard ran for exactly as long as somebody left a terminal open (#261). watch install hands the monitor to whatever the host already supervises and serve had no such path, so the dashboard was never up at the moment it would have been useful. homebutler serve install writes the same kind of user-level unit β€” a launchd agent, a systemd user unit β€” on 127.0.0.1:8080 unless --host and --port say otherwise, and serve uninstall is named in its own help text. Reinstalling on a different port needs --force and stops the running service first, because overwriting the file leaves launchd serving the old address. The token is not in the unit: a unit file is world-readable and --token is in ps for every user on the machine, so it is read from web.token in the config file β€” which serve itself now reads too. Installing on an address other machines can reach with no token configured is refused, and says both of the two things that fix it. serve installed reports the address by reading it back out of the unit the supervisor runs, so it cannot name a port nothing is listening on

  • the Docker card can restart and stop a container, and the two do not ask the same way (#272). #242 gave fourteen actions routes, tiers and handlers, and no screen called any of them. Restart runs on the click that asked for it β€” the container comes back, and a second dialog would be asking about something that undoes itself. Stop asks once, and says what it is asking: the container will stay stopped and the dashboard has no way to start it again, because there is no start tool. That wording is the point. "Are you sure?" is the dialog people learn to click through, and the tier below this one β€” where backup_restore and install_purge need the target's name typed back β€” depends on them not having learned that here. Without a token no action appears at all: the routes are not registered, so a button would answer 404, which the screen cannot tell apart from a feature that does not exist

  • the watch list can be changed from the screen that shows it (#272). Adding a target, removing one and checking every target now are the first tier β€” a target removed by mistake is a target added back, and a check that was not needed costs one poll β€” so all three run on the click that asked for them and none of them stops to ask. Two answers that were not errors now read as what they are: watch add for something already on the list says so instead of reporting a save, and watch check says whether it found anything rather than only that it ran. Adding a target while no service is installed says the obvious thing out loud, because the list records nothing until something polls it

  • two actions were reachable, correct, and impossible for anyone but our own dashboard to call (#272). backup_restore takes the name of an archive and no route returned one. install_purge takes the name of an app and no route listed them. Our dashboard would have known those values because it fetched them from somewhere; any other caller had nowhere to fetch them from, and that was about to be frozen at 1.0. backup_list, install_list, install_status and proxmox_guests have HTTP routes now. Every route that acts on a named thing declares where the name comes from, and a test fails when that source is not a read this API exposes β€” the rule being that an endpoint taking an identifier exists alongside one producing it. The other thirteen absences stay absent: the line is not whether a read has a sibling but whether the write beside it can be used without it

  • the dashboard has an Apps tab, and removing an app's data asks for its name (#272). The catalogue, what each app would bind, and installing, uninstalling and purging one β€” the three tiers on one screen. Installing is not a button: the pre-flight runs first, and a port that is already taken comes back as "port 9443 is already in use by nginx" beside the field that fixes it, because that is an answer and not a failure. Uninstall and purge are deliberately different. Uninstall stops the app and leaves its data, and runs on the click. Purge deletes the data and asks for the app's name typed back, because a click cannot say which thing the operator meant to lose. A status is fetched when a row is opened rather than for all fifteen apps on every visit

  • Proxmox guests can be started, rebooted and shut down from the card that lists them (#272). The last of the fourteen. Start and reboot run on the click β€” a started guest can be shut down and a rebooted one comes back β€” and shutting one down asks once, saying that it stays off until somebody starts it again. A guest is addressed explicitly and never guessed: the vmid goes in the path and the node and type in the body, because the same vmid can exist on two nodes of a cluster. A template offers nothing, because Proxmox would refuse and a button that cannot work is worse than no button

  • doctor now notices a dashboard that has drifted into answering the network with no token (#268). serve install refuses to bind past loopback without one, and that is a decision made once β€” the config file outlives it. Remove web.token afterwards and a dashboard installed on 0.0.0.0 with authentication keeps running without it: nothing fails, the service is not restarted, and the write routes stop being registered rather than start refusing, so from outside it is indistinguishable from one that was always read-only. It is a fail, not a warning, and the hint names both ways out because which is right depends on whether it was meant to be reachable. A unit that states no address at all is reported as something homebutler could not judge rather than assumed safe. doctor does not ask the supervisor whether the service is up β€” that would mean opening a socket, and this is the command people run when something is already wrong

πŸ› Fixes

  • an app that had simply never been installed answered HTTP 500 (#272). GET /api/install/{app} passed install.Status's error straight through, and that error is what it returns for an app with no compose file β€” which is the ordinary state of fourteen of the fifteen apps in the catalogue. It answers {"installed": false} now. The route shipped one release earlier than the screen that would have asked it, which is how nobody found out

  • config validate would have gone on ignoring any section added to the config (#261). topLevelKeys says it mirrors the yaml tags on Config, and nothing read the other half of th...

Read more