Skip to content

feat: add a menu bar app that shows and controls the stack - #187

Merged
arthware-dev merged 19 commits into
mainfrom
feat/menubar-app
Oct 7, 2026
Merged

arthware-dev merged 19 commits into
mainfrom
feat/menubar-app

Conversation

@arthware-dev

@arthware-dev arthware-dev commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add a macOS menu bar app (beta, apps/macos/): what needs attention with the command that fixes it, each stacklet with start, stop, restart, open and logs, errors from the last 24 hours, disk and memory, and a Setup tab. It is a client of the stack CLI only, on this Mac or on another one over SSH.
  • Add ./stack app install: builds the app from the checkout, puts it in /Applications and opens it; again after ./stack update, which says so when the app is installed.
  • Add the CLI commands the app reads: stack errors, stack host, stack doctor --json, stack config --json with credentials hidden, and each stacklet's address in list and status. The admin guide describes installing and connecting the app.

Commits stand alone: rebase merge.

A native SwiftUI menu bar app in `apps/macos/`, built with the Command
Line Tools alone (`apps/macos/build.sh`). It is a client of the stack
CLI: it reads `./stack status --json` every minute and when the panel
opens, and runs `./stack up|down|restart <id>` in the background.

- one row per installed stacklet with its state, port and health issues
- start, stop, restart and open in the browser from the row
- first installs, logs and doctor open in Terminal, since they prompt
  or never end
- restart for stacklets running code the checkout has moved past
- disk and memory from the host figures in the status

The app starts no services and keeps no state beyond the chosen
checkout. It is a prototype: not built in CI and not distributed.
`stack doctor` found a stacklet's containers by the name pattern
`stack-<id>-*`. A stacklet with one service is named `stack-<id>`, so
doctor never saw the code stacklet's Forgejo container: an exited or
drifted `stack-code` went unreported.

Containers are now matched by their compose project, `stack-<id>`,
which every container of a stacklet carries whatever its name.
`stack doctor --json` prints the running version, the one-line summary
and every finding with its level, title, detail and fix command. The
exit code is unchanged: 1 when any finding is an error.
`stack errors` reads the log of every stacklet container over the last
24 hours and lists each container that logged errors, how many, and the
newest three lines. `--since 30m` or `--since 168h` changes the window;
`--json` gives the newest five per container with their timestamps.

A line counts when its log format marks it as an error: an `ERROR`,
`CRITICAL`, `FATAL` or `PANIC` level, Forgejo's `[E]`, nginx's
`[error]`, `level=error`, or the start of a Python traceback. Lines
that only mention an error in their message are not counted.

The README and the admin guide have listed this command for a while;
it now exists.
`stack host` prints the machine's memory, disk and uptime, and each
stacklet's memory and CPU summed over its containers, largest first.
`--json` gives the same for scripts.

The per-stacklet figures come from `docker stats`, which samples for
about two seconds. A stacklet that runs on the host rather than in
containers, such as the AI server, has none.
The panel now opens with a verdict and a list of what needs the admin:
`stack doctor` findings, backups that are missing, failed or overdue,
and a disk above 90%. Each item shows its fix command; `up`, `restart`
and `down` run in the background, anything else opens in Terminal.

Below it: memory per stacklet, the containers that logged errors in the
last 24 hours with their newest line, the last backup, and disk, memory
and uptime. These come from `stack doctor --json`, `stack errors`,
`stack host` and `stack backup status`, read when the panel opens and
every ten minutes.

The icon warns only on errors, so a standing warning such as "no backup
set up" does not keep it lit. Against an older checkout without those
commands, their sections are left out.
`stack config` read `stack.toml` from the checkout even when `STACK_DIR`
points the CLI at another instance, so it printed the wrong file or
none. It now reads the file every other command uses.
`stack config --json` prints both files as written, with the paths to
them, for scripts and the menu bar app. Any key named like a credential
(ending in `key`, `password`, `secret` or `token`) is replaced by
`(hidden)` wherever it appears, so an AI server's API key or a password
in users.toml never leaves the file. An empty credential stays empty,
so set and unset can still be told apart.
A collapsed Setup section lists the family from users.toml, port or
domain mode, language and time zone, the AI server and model, the
update time and the data dir, read from `stack config --json`. Two
buttons open `stack.toml` and `users.toml` in the text editor; doctor
then names what still runs with the old setting.

A missing backup is no longer reported: the backup stacklet is in beta.
A failed or overdue run is still reported once it is installed.
The panel had grown taller than the screen. Overview keeps what an
admin checks often: what needs attention, the stacklets, recent errors,
disk and memory. Setup holds what changes rarely: the configuration,
the stacklets not installed yet, and the checkout with its version.

Sections are cards with more room between rows, stacklet rows show
memory with the port moved into their menu, and the panel scrolls
once it reaches the height of the screen.

`StackMenu --snapshot <prefix>` renders both tabs from the real data to
PNG files, to check a layout without opening the menu.
The menu bar shows the "a" with its two dots in monochrome, so macOS
tints it like every other menu bar icon. State stays monochrome: a
badge dot when something needs attention, hollow dots while an action
runs, a dimmed mark when the CLI does not answer.

The app icon is the dark tile of the website's favicon, rendered at
build time from the same drawing. The outline of the "a" is generated
once from Inter Bold (SIL Open Font License) by
`apps/macos/script/logo-outline.py`, so the app ships no font.

`--snapshot` also renders the icon states, and neither tool mode puts a
second icon in the menu bar any more.
`stack list --json` and `stack status --json` now give every stacklet
with a web port a `url`: `http://<LAN address>:<port>` in port mode,
`<scheme>://<id>.<domain>` in domain mode. It is the address the stack
already hands to its stacklets as `{url}`, so a client on another
machine can open a stacklet without rebuilding that rule.
A switch in Setup chooses between a checkout on this Mac and one on
another Mac, and both are remembered. Remotely the app runs
`ssh <host> 'zsh -lc "cd <checkout> && ./stack …"'` with the admin's
SSH key and never prompts, so a server with no screen can be watched
from a laptop. Logs, doctor and editing open Terminal with an SSH
session to that Mac, and stacklets open at the address the stack names
for them.

Every CLI call now stops after a minute, lifecycle actions after
fifteen, so an unreachable or hanging machine shows an error instead of
a panel that never updates. Answers that arrive after a switch of
machine are dropped.
A failed read or action now says what happened and what to do instead
of showing raw output:

- the CLI's own error reply, or its last lines, for a failed command
- one message for an SSH failure (ssh exits 255) with ssh's own line
  and a button that tests the connection in Terminal
- a missing checkout on the remote Mac, a checkout older than the app
  (argparse exits 2), a command that did not answer in time
- a success confirms itself for a few seconds

A command is done when its process exits, not when its pipes close, so
a docker child holding them no longer hangs a read. Reads stop after a
minute (SIGKILL if SIGTERM is ignored); an action past fifteen minutes
keeps running and reports when it ends. One action runs at a time and
shows its elapsed time. Reads asked for during another read run after
it, the panel marks an old state as old when the machine stops
answering, and it reads again after the Mac wakes.

A report without JSON never shows its stdout: an older CLI that
ignores `--json` can print the whole stack.toml there. The SSH host is
checked before it becomes an argument, and cancelling the remote
dialog leaves the switch on This Mac.
`stack doctor` derived the stacklet for `stack logs` from the container
name. A container left from an older version of a stacklet keeps its old
name, so `famstack-wiki-preview-de` in the memory project got
`stack logs wiki`, a command for a stacklet that does not exist.

The fix names the stacklet the container belongs to.
The panel said "up 0 days" for a Mac started today and "up 1 days" a day later. It gives hours for the first day, then "1 day" and "n days".
The bundle said 0.1.0 whatever the release. `build.sh` takes the version from the CLI, without a release candidate's label, which macOS does not accept there, and uses the commit count as the build number, so a release candidate and its release differ.
The admin guide gets a section on the menu bar app: what it shows, how to build and install it from the checkout, and how to connect it to the server Mac over SSH. The app's README calls it beta and names the copy step; the operator guide says to rebuild it after an update.
The menu bar app is signed for the Mac that builds it, so it ships as
source. Installing it took the build script and a copy by hand, and an
update meant knowing to do both again.

`./stack app install` builds the app from the checkout, puts it in
`/Applications` (`~/Applications` when that is not writable), replacing
an older build as a whole, and opens it. Running it again updates the
app, and `./stack update` says so when the app is installed.
@arthware-dev
arthware-dev merged commit b98222d into main Oct 7, 2026
2 checks passed
arthware-dev added a commit that referenced this pull request Oct 7, 2026
A macOS menu bar app (beta, `apps/macos/`) shows what needs attention
with the command that fixes it, each stacklet with start, stop, restart,
open and logs, errors from the last 24 hours, the Mac's disk and memory,
and a Setup tab. It is a client of the `stack` CLI only, on this Mac or
on another one over SSH. `./stack app install` builds it from the
checkout, puts it in `/Applications` and opens it; running it again
after `./stack update` updates it.

The CLI gains the commands the app reads:

- `./stack errors`: recent error lines across every stacklet
- `./stack host`: memory, disk, uptime and what each stacklet uses
- `./stack doctor --json` and `./stack config --json`, credentials hidden
- each stacklet's address in `./stack list` and `./stack status`

`./stack doctor` checks stacklets that run a single container (Forgejo
went unreported) and points an exited container's fix at its
stacklet's logs. The admin guide describes installing and connecting
the app.
@arthware-dev
arthware-dev deleted the feat/menubar-app branch October 7, 2026 11:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant