From c27d9f0adaa0518ebcc34b3725cda7041a74f188 Mon Sep 17 00:00:00 2001 From: Mitchell-Johnson Date: Sat, 10 Oct 2026 14:53:10 -0700 Subject: [PATCH] Stacks: one machine, one set of files; default home is /stacks The hub no longer probes a node's Docker on tcp :2375, holds compose files for a node, or runs compose against another machine's Docker. hubStacks, nodeDocker, taskOnHub and the hub-side /api/nodes/{node}/docker/... routes are gone; the generic node proxy hands those requests to the node's own Ham Lab. Stack.remote / held_by and compose.Spec.Remote are removed. New stacks default to /stacks on every machine. Folders with two or more deployed stacks are still offered, after it, never as default. The ~/stacks fallback is gone. A missing home is skipped without a sudo find. A schedule saved earlier with a tcp target is refused with a message instead of being run against this machine's Docker. PORTABILITY.md 3.1, 3.2, 3.3 and 9.2 done; 3.5 checked, no change needed. Co-Authored-By: Claude Opus 5.5 --- PLAN.md | 34 ++-- PORTABILITY.md | 14 +- internal/api/dockersched.go | 12 +- internal/api/server.go | 22 +-- internal/api/stacks.go | 353 ++++++++---------------------------- internal/api/stacks_test.go | 35 ++++ internal/compose/compose.go | 20 +- web/src/Docker.tsx | 4 +- web/src/StackNew.tsx | 2 +- web/src/api.ts | 4 +- 10 files changed, 160 insertions(+), 340 deletions(-) diff --git a/PLAN.md b/PLAN.md index cc8c62b..5c491fe 100644 --- a/PLAN.md +++ b/PLAN.md @@ -21,7 +21,7 @@ stub tab or a roadmap item. | Guest logins | A **guest** is an account like any other, with `guest: true` and an expiry: `login`, sessions and prefs need no special case, and **it may do everything an account may do**. The expiry is the whole feature — checked on every request and swept once a minute — with revoking as the manual version of it. | Built 2026-09-27. A read-only version was built first and **cut the same day**: Mitchell hands these to people he trusts and revokes them otherwise, so a console with half its tabs missing was solving a problem he doesn't have. Consequences, stated once and accepted: a guest has the Terminal tab, which is a root-capable shell on every host, and can make an account or token of its own that outlives the guest login. The one thing a guest is not is *an account*: it is left out of the Users list, out of "the last account can't be removed", and out of the single account an empty user name resolves to. | | Machines | **A view shows one machine: the one in the switcher.** No hub-side page lists or aggregates what its nodes are doing. To see or change anything on another machine, switch to it. | Mitchell, 2026-09-25, cutting a fleet-wide updates list out of the Docker tab. The switcher is the only place a machine is chosen, so there is exactly one answer to "which machine am I looking at" — a page that quietly reaches across breaks that, and it is never obvious which machine an action would land on. The hub still *proxies* for a node, and hub-only admin (Machines, Users, Settings, Alerts) is unaffected. | | Capability, not policy | **A tab offers what the selected machine can do, detected live — never what a rule says should run there.** Where the software is missing but installable, Ham Lab installs it; where it cannot be installed or the hardware lacks it, the tab says so in a sentence and offers what does work. Every action lands on the machine in the switcher. | Mitchell, 2026-10-09, after the Virtualization plan's first draft excluded the Mac mini and `hamtv` on the strength of `hamlet/CLAUDE.md`'s deployment rules: *"I want the option to do it anywhere I want, unless the hardware / software literally doesn't support it. Ham Lab needs to be built in a general-purpose way that can run on any Linux system."* Those rules bind the agent's unasked deployments, not the product. | -| Ham Lab's data lives in Ham Lab's folder | **Everything a tab manages sits under the machine's data directory (`config.DataDir()`), never at a convenient path elsewhere.** Existing material is imported by copying. The engine's runtime state (`/var/lib/docker`, `/var/lib/incus`) is the engine's; Ham Lab keeps what would rebuild it. | Mitchell, 2026-10-09, when the Virtualization plan pointed its ISO library at `/tank/software/isos/`: *"ALL data needs to be under a HamLab folder, because it manages its own data."* The same reason `core`'s compose files moved under `apps/hamlab/stacks/` on 2026-10-05. `grid`'s and the Mac's stacks at `~/stacks//` are the standing exception. | +| Ham Lab's data lives in Ham Lab's folder | **Everything a tab manages sits under the machine's data directory (`config.DataDir()`), never at a convenient path elsewhere.** Existing material is imported by copying. The engine's runtime state (`/var/lib/docker`, `/var/lib/incus`) is the engine's; Ham Lab keeps what would rebuild it. | Mitchell, 2026-10-09, when the Virtualization plan pointed its ISO library at `/tank/software/isos/`: *"ALL data needs to be under a HamLab folder, because it manages its own data."* The same reason `core`'s compose files moved under `apps/hamlab/stacks/` on 2026-10-05. `grid`'s and the Mac's stacks at `~/stacks//` were the standing exception; since 2026-10-10 the stacks code offers `/stacks` as the default home everywhere and such folders only as discovered extras. | ## Layout @@ -260,12 +260,13 @@ something long in one, switch away and back, refresh — it's all still there. filesystem; compose is then run **inside that container**, so `.env`, relative paths and the compose labels stay exactly as deployed (verified: labels identical after a redeploy). Save validates with `compose config` first, keeps the previous file as - `.hamlab-bak`, and never deploys by itself. A stack deployed from another host - (Dockhand on `core` driving `grid` over tcp) has no file on that machine, so the node - alone lists it read-only — but **the hub fills that in** (`hubStacks`): asked about a - node, it resolves the files on itself and runs compose here with `DOCKER_HOST` pointed - at the node's `tcp://…:2375`, the way it was deployed; the task runs on the hub and the - node's page follows it. Also per stack: every file it was deployed from (chips), its + `.hamlab-bak`, and never deploys by itself. A stack whose file is not on the + machine that runs it is listed read-only, and that is the end of it: **since 2026-10-10 + the hub holds no files for a node and never points compose at another machine's + Docker.** The `hubStacks` path — which resolved a node's files on the hub and deployed + over `tcp://…:2375`, the way Dockhand had — is gone, and every + `/api/nodes//docker/…` request is the node's own Ham Lab's to answer through the + generic proxy. Also per stack: every file it was deployed from (chips), its **`.env`** (values masked until "Reveal and edit"; validated in place, put back if compose objects), **what Deploy would change** (a line diff against the copy kept at the last deploy from here, `.hamlab-deployed`, else the copy the last save replaced; @@ -278,9 +279,10 @@ something long in one, switch away and back, refresh — it's all still there. form (image, ports, volumes, environment, restart, command, host networking) that writes the compose for you: a container made here is a one-service stack, so it stays editable and redeployable, where a bare `docker run` could only be thrown away and typed again. - New stacks go in a **home** — a folder where two or more stacks are already deployed from - (Dockhand's `stacks/core` here; `~/stacks` on a machine with none) — and are deployed the - way their neighbours were (inside `dockhand`, or over tcp to a node from the hub). Homes + New stacks go in a **home**. Since 2026-10-10 the default is Ham Lab's own + `/stacks`, offered first on every machine; a folder where two or more stacks + are already deployed from is offered as well, and a stack made there is deployed the way + its neighbours were (inside an orchestrator's container, when that is how they run). Homes are also scanned, so a stack with no containers ("not deployed": never deployed, or taken down) is still listed. **Down** removes a stack's containers; **Delete…** (type its name) takes it down and moves its own files to `/.hamlab-trash/-