Repository navigation
Releases: Higangssh/homebutler
Release list
v1.2.0
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 purgeremoves 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 everydocker compose upafter 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 printedRemoved, except: kept volume β¦ (still exists)twice rather thanCompletely 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.ignorenames containers whose stopping and starting is intended, such as one a cron job stops every night (#338).watch startandalerts --watchreport 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 validatelists the ignored names, and saving a threshold from the dashboard keeps the list
π Fixes
- every container and threshold alert from
watch startreached its channel stamped0001-01-01 00:00:00(#341).watchpassed 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 validatewarned thatalerts.ruleswas an unknown key with no effect, whilewatchwas acting on those rules (#338). The validator checked against a schema that has no rules field, andwatchreads them through a different struct. It now checks each rule against the typewatchdecodes into: a misspelt key inside a rule is reported at its line, and a rulewatchwould refuse is reported as an error. A test fails ifwatchreads a key thatvalidatedoes not accept
v1.1.1
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 --alland the dashboard's server overview ask every server at once, so that is 0.8 s per call, not per server
v1.1.0
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
reportno longer compares the processes that belong to a container or towatch(#315). Recreating one container used to produce seven lines where two said anything. Each container'scontainerd-shimcarries 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, whichpsgives on Linux, macOS and busybox alike. Thedocker eventshelperwatch startruns is left out too, so an upgrade that changes its arguments is no longerreplaced: docker. Thewatchprocess itself is still compared. Whatreportgives 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 reportedgonethat onceinstall purgenow 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/configand/databaseas volumes that the compose file does not map, so Docker gave each container an anonymous volume, andcompose downkeeps those. Purge now runsdown -vand lists each volume it removed. It saysCompletely removedonly when Docker confirms every one is gone. A volume that still exists (external, or used by another container) is listed as kept underRemoved, 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_purgeover MCP andPOST /api/install/{app}/purgealready answereddata_preserved: false, and now that is true.uninstallis unchanged and keeps them
v1.0.0
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
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
upgradereplaced 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 stamped0.41.1-devwas moved tov0.41.0, and a machine already on a newer tag was moved back while the latest release was still the previous one. A higherMAJOR.MINOR.PATCHis 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. Amake buildis stamped bygit describe, andv0.41.2-2-gc180322orv0.41.2-dirtycounts 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 localdevbuild with no numbers is still refusedupgradeprinted twovs in front of a version that already had one (#313):already vv0.41.2for everymake buildexactly at a tag, sincegit describestamps thevitselfreportsaid a port had a new owner when only its container's image had changed (#314). A Docker container owns a port under the labelname (image), and the comparison used the whole label, so a container whose image tag moved β or one recreated on a new image β was reported asport: :8082/tcp β it-tools (corentinth/it-tools:latest) β it-tools (8b8128748339), with aneeds_attentionitem saying the port "was not" answered by it at the last report. The image change is already theimageline, and a recreated container isreplaced. 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.0and[::]that changed hands produced itsneeds_attentionitem and its suggested action twice (#314), once per listener.notable_changesalready counted it once
π¦ Distribution
v0.41.2
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 oneskippedline, 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;--jsonand the MCP tool now return[]. Found on the 0.40.0 soak, whose first quiet day saidskipped. A first run and an unreadable previous snapshot still carry theirskippedline, because for them the comparison really was not made needs_attentionandsuggested_actionsare[]when there is nothing in them, notnull(#308), and so isnotable_changes.nullsays the field is missing;[]says there is nothing in it. A caller that checked== nullfor "nothing to do" now has to check for an empty listinstall_list,GET /api/installand awatchincident'sflappinguse snake_case keys (#309). They were the only answers with capitalised keys:Name,DefaultPortand the rest for an app,IsFlapping,Countand the rest for flapping, because neither struct had JSON tags.install_listalso 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_historyon a machine with no history answerednullover MCP while--jsonprinted[]for the same thing, and a list nested in an answer βsystem.diskswhen no disk could be read β could benullwherever nothing had been appended to it. MCP, the HTTP routes and--jsonnow pass every answer through one function that writes an empty list or map wherenullwould have been, at any depth. A field withomitemptyand a nil pointer still mean "absent" and are left alone
π Fixes
- the dashboard's report card showed a bare
skippedchip on a quiet day (#308). It now says No significant changes since last report. when the list is empty
v0.41.1
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
binthat starts with~or$HOMEnow 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. Abinlike~/.local/bin/homebutlernow reaches the binary it names - A server's
binthat 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$HOMEis expanded
π Fixes
deployfailed on every account that could not write/usr/local/bin, and said onlyProcess exited with status 1(#303, #304). Since 0.10.0 quoted remote paths against shell injection, the fallback$HOME/.local/binreachedscpquoted 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 ininternal/remotefails when a path is quoted without going through the helper that leaves a leading~or$HOMEto the remote shell. The account with passwordless sudo failed too:deploychose/usr/local/binfor it and then uploaded there without sudo. It now uploads to a temporary file and moves it in withsudo -n install. Reported by @wardbryan3, with a reproduction- an upload
scprefused 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, anddeployandupgradereport the line scp gave:scp: /home/bob/.local/bin/homebutler: Permission denied deploychecked the install by running whicheverhomebutlercame first onPATH(#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/binline is not given a second oneupgradestopped atPermission deniedon a remote binary it could not write, without saying what would (#305). Since #304deployinstalls through sudo where the account has it, so the binary can be root's, andupgradewrites as the login user. It now names the path and the user and points tohomebutler 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
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
watchincident that ended in exit 137 isoomonly when the OOM killer was involved (#295). Every 137 used to beoomwithhighconfidence, sodocker kill, adocker stopthat ran out of time andkill -9were all filed as OOM. The Docker monitor subscribed todiealone and never received theoomevent Docker sends just before an OOM death. It now subscribes tooomtoo: a die that follows its container'soomwithin ten seconds is OOM-killed, withdocker inspectas a second source, and a 137 without that evidence isunknown,low, signalSIGKILL. An agent that branched onoomnow receives it only for an OOM. Found on the 0.40.0 soak: adocker killcame back asoom/highwhiledocker inspectsaidOOMKilled=false - An incident's
prev_started_atandcurr_started_atare 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 saiddied at event time 1790772824and(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_atis now the start of the run that died andcurr_started_atthe 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 arepm_uptime, checked against a real pm2 around a restart; the restart count was already inrestart_count
π Fixes
installfinished by suggestinghomebutler logs <app>, a command that does not exist (#296). It ishomebutler 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 missinghomebutler tuiin #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 gotunknown command "logs"watch installandserve installtold you to enable lingering when it was already on, and to use sudo for it (#296). They now askloginctlfirst and say nothing when lingering is on. When it is off, the command they give isloginctl enable-lingerwithout sudo, which systemd allows for your own account by default, with sudo as the fallbackwatch listshowed LAST CHECKED frozen at the moment a target was added while the watch service was watching it all along (#297). The column is the lastwatch addorwatch 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.categoryandconfidenceare 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 wrongoomcould have been frozen
v0.40.0
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_logslines,processeslimit,watch_historylimit,install_appport,reportkeepanddoctorbackup_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.keepandbackup_max_age_hourswerenumber, 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
typeasqemuorlxc(#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 --jsonandstatus --jsoncarryschema_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: thesystemobject nested insidereportandinventory_scandoes not carry one, because there it would read as the version of the document around itreportwarns 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, butwarningsnow names the file and the reason, and the note says nothing was compared. A first run, with no snapshot at all, says nothing newproxmox_nodeleaves out a count or flag Proxmox did not send (#291).cpuinfo.cores,cpuinfo.cpus,cpuinfo.user_hzandboot_info.securebootwere written as0orfalsewhen 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,0andfalseincluded
β¨ 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
backuptarred it whole, every time β reported from a real Jellyfin setup where that directory is terabytes.--excludeleaves a path out, covering anything mounted under it on path boundaries, so/mnt/mediacovers/mnt/media/moviesand 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 markedexcludedrather 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--excludearchives 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 drillboots 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
vmidas 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 oneproxmox.GuestActionResultnow, and all four use it -
proxmox_statusnever showed the repository id Proxmox reports (#291). Proxmox sends it asrepoid, 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,doctorandstatusalone, so a field renamed insidedocker_listorwatch_listβ the answers an agent branches on most β passed. #283 added three fields toBackupResultand the golden did not move.docs/compatibility.mdnow 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
statuswas a string and not which strings, soinstall_purgecould have stopped answeringpurged, orwatch_addcould have stopped takingpm2, 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.guestsrenamesmaxcputomax_cpuand splitstagsinto a list. The guest actions take theirvmid,nodeandtypefrom 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_collectorsis 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
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 --tokento edit alert thresholds asked for that, and the token is typed once, lives inlocalStorage, and belongs to whoever opened the page. Before upgrading, look again at who knows it β or install the dashboard on127.0.0.1, where it answers only the machine it runs on -
Four capabilities that recorded
no view built for it yetnow have HTTP routes.backup_list,install_list,install_statusandproxmox_guestsanswerGET /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_guestswas 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_stopandproxmox_guest_shutdowntake an explicitconfirm, because the service is down until somebody starts it.backup_restoreandinstall_purgetake 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
tokenand 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 installhands the monitor to whatever the host already supervises andservehad no such path, so the dashboard was never up at the moment it would have been useful.homebutler serve installwrites the same kind of user-level unit β a launchd agent, a systemd user unit β on127.0.0.1:8080unless--hostand--portsay otherwise, andserve uninstallis named in its own help text. Reinstalling on a different port needs--forceand 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--tokenis inpsfor every user on the machine, so it is read fromweb.tokenin the config file β whichserveitself 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 installedreports 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_restoreandinstall_purgeneed 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 addfor something already on the list says so instead of reporting a save, andwatch checksays 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_restoretakes the name of an archive and no route returned one.install_purgetakes 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_statusandproxmox_guestshave 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
vmidgoes in the path and thenodeandtypein the body, because the samevmidcan 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 -
doctornow notices a dashboard that has drifted into answering the network with no token (#268).serve installrefuses to bind past loopback without one, and that is a decision made once β the config file outlives it. Removeweb.tokenafterwards and a dashboard installed on0.0.0.0with 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.doctordoes 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}passedinstall.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 validatewould have gone on ignoring any section added to the config (#261).topLevelKeyssays it mirrors the yaml tags onConfig, and nothing read the other half of th...