feat(maestro): serve the web UI from the daemon

The browser reached the daemon through maestro-web, a second binary that
served the SPA and reverse-proxied /api/* — two TCP connections and a
WebSocket frame re-encode on every keystroke and every byte of PTY output.
On one machine that hop buys nothing.

The daemon now embeds the bundle and serves it off its own listener, behind
a default-on `web` feature. `--no-default-features` builds without pnpm or
node; MAESTRO_WEB_DIST still takes a prebuilt bundle.

The router's fallback answers an unmatched /api/* with 404 rather than the
SPA shell, so a client expecting JSON gets a status it can read instead of
HTML.

Removes maestro-web and the [web] config section. An existing [web] block
is ignored, not rejected. The installer deletes a leftover
maestro-web.service on upgrade.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Miguel Palhas
2026-09-02 18:23:18 +01:00
co-authored by Claude Opus 5
parent 2ad2858fd5
commit 4fc90aabfb
35 changed files with 528 additions and 1596 deletions
+47 -68
View File
@@ -1,8 +1,8 @@
# Home Manager module: `services.maestro`.
#
# Maestro owns the user's interactive shells and tmux sessions, so both units
# are *user* services, not system ones — same trust boundary as the tmux server
# they wrap.
# Maestro owns the user's interactive shells and tmux sessions, so the daemon
# is a *user* service, not a system one — same trust boundary as the tmux
# server it wraps. It serves the browser UI too; there is no second unit.
{ self, ... }:
{
flake.homeManagerModules.default =
@@ -27,6 +27,18 @@
];
in
{
imports = [
(lib.mkRemovedOptionModule [ "services" "maestro" "web" "enable" ] ''
The daemon serves the browser UI off its own listener now, so there is
no second unit to enable. Point a browser at
`services.maestro.settings.daemon.bind_addr` instead.
'')
(lib.mkRemovedOptionModule [ "services" "maestro" "web" "package" ] ''
The `maestro-web` package is gone; the SPA is embedded in the
`maestro` binary under its default-on `web` cargo feature.
'')
];
options.services.maestro = {
enable = lib.mkEnableOption "Maestro — installs the CLI/TUI and, unless disabled, runs the daemon";
@@ -43,94 +55,61 @@
description = "Run `maestro daemon` as a systemd user service.";
};
web = {
enable = lib.mkEnableOption "the `maestro-web` browser UI as a systemd user service";
package = lib.mkOption {
type = lib.types.package;
default = self.packages.${pkgs.stdenv.hostPlatform.system}.maestro-web;
defaultText = lib.literalExpression "maestro.packages.\${system}.maestro-web";
description = "The `maestro-web` binary, with the SPA bundle embedded.";
};
};
settings = lib.mkOption {
type = tomlFormat.type;
default = { };
example = lib.literalExpression ''
{
daemon.bind_addr = "127.0.0.1:7070";
web.bind_addr = "0.0.0.0:8081";
}
'';
description = ''
Contents of `~/.config/maestro/config.toml`, in the schema
`maestro-config` defines. Every field has a default, so `{}` is
valid and writes no file. There is no auth layer — bind the web UI
to a LAN/VPN address only on a trusted segment, never a public IP.
valid and writes no file. `daemon.bind_addr` is also the address the
browser UI is served on. There is no auth layer — bind it to a
LAN/VPN address only on a trusted segment, never a public IP.
'';
};
};
config = lib.mkIf cfg.enable {
home.packages = [ cfg.package ] ++ lib.optional cfg.web.enable cfg.web.package;
home.packages = [ cfg.package ];
xdg.configFile."maestro/config.toml" = lib.mkIf (cfg.settings != { }) {
source = tomlFormat.generate "maestro-config.toml" cfg.settings;
};
systemd.user.services = lib.mkMerge [
(lib.mkIf cfg.daemon.enable {
maestro-daemon = {
Unit = {
Description = "maestro daemon — tmux wrapper + session API";
After = [ "network.target" ];
# MUST stay 0: a fast-crashing daemon would burn the default
# 5-starts-per-10s budget and park the unit in `failed` until a
# manual `systemctl --user reset-failed`.
StartLimitIntervalSec = 0;
};
Service = {
Type = "simple";
WorkingDirectory = "%h";
ExecStart = "${lib.getExe cfg.package} daemon run";
Environment = [
"PATH=${profilePath}"
# Keeps the daemon on the same tmux server the user's shell
# and TUI use, instead of a second one under /tmp.
"TMUX_TMPDIR=%t"
];
Restart = "always";
RestartSec = 2;
# If this unit boots before any shell, the shared tmux server
# lands in its cgroup; the default control-group kill would take
# every session down on restart.
KillMode = "process";
};
Install.WantedBy = [ "default.target" ];
systemd.user.services = lib.mkIf cfg.daemon.enable {
maestro-daemon = {
Unit = {
Description = "maestro daemon — tmux wrapper + session API + browser UI";
After = [ "network.target" ];
# MUST stay 0: a fast-crashing daemon would burn the default
# 5-starts-per-10s budget and park the unit in `failed` until a
# manual `systemctl --user reset-failed`.
StartLimitIntervalSec = 0;
};
})
(lib.mkIf cfg.web.enable {
maestro-web = {
Unit = {
Description = "maestro-web — browser UI for the local maestro daemon";
After = [ "network.target" ] ++ lib.optional cfg.daemon.enable "maestro-daemon.service";
Wants = lib.optional cfg.daemon.enable "maestro-daemon.service";
StartLimitIntervalSec = 0;
};
Service = {
Type = "simple";
WorkingDirectory = "%h";
ExecStart = lib.getExe cfg.web.package;
Environment = [ "PATH=${profilePath}" ];
Restart = "always";
RestartSec = 2;
};
Install.WantedBy = [ "default.target" ];
Service = {
Type = "simple";
WorkingDirectory = "%h";
ExecStart = "${lib.getExe cfg.package} daemon run";
Environment = [
"PATH=${profilePath}"
# Keeps the daemon on the same tmux server the user's shell
# and TUI use, instead of a second one under /tmp.
"TMUX_TMPDIR=%t"
];
Restart = "always";
RestartSec = 2;
# If this unit boots before any shell, the shared tmux server
# lands in its cgroup; the default control-group kill would take
# every session down on restart.
KillMode = "process";
};
})
];
Install.WantedBy = [ "default.target" ];
};
};
};
};
}