Mark Phase 3 (Spark Control) done; trim spec to live command contract

Shipped in Spark Control v0.21.0: status badge + Update/Restart/Stop-Start/Logs
tile. All three exit criteria confirmed. matrix-bridge needed no code change.

- AGENTS.md: Current state + ROADMAP Phase 3 -> DONE; Deploy switched scp -> git
  pull (Update button); D10 stamped; new Infra fact for the Spark->Gitea path and
  the load-bearing IdentitiesOnly ssh-config pin the Update button depends on.
- spark-control-integration.md: trimmed from dev spec to live contract (dropped
  sudo -iu fallback and dev-side scaffolding; folded in direct-as-modelo, the
  Gitea key gotcha, restart cadence, and the LAN-only HTTP API).
- README: dropped stale "pre-Phase 0" status; Setup reframed for a fresh install.

Deferred follow-up: badge reflects container liveness only, not Matrix
connectivity; HEALTHCHECK + {{.State.Health.Status}} is the matrix-bridge-side fix.
This commit is contained in:
Keysat
2026-06-15 23:19:30 -05:00
parent 843582ec03
commit 28c974fe1d
4 changed files with 123 additions and 139 deletions
+45 -19
View File
@@ -61,20 +61,23 @@ the full answer back into the room (ask mode, D12).
- **Bot — venv (dev/fallback):** `python3 -m venv .venv && .venv/bin/pip install -r requirements.txt`, - **Bot — venv (dev/fallback):** `python3 -m venv .venv && .venv/bin/pip install -r requirements.txt`,
then `.venv/bin/python src/bot.py` — uses modelo's host `~/.ssh/config` for the alias. then `.venv/bin/python src/bot.py` — uses modelo's host `~/.ssh/config` for the alias.
`MB_SSH_ALIAS` overrides the SSH target for testing. `MB_SSH_ALIAS` overrides the SSH target for testing.
- **Deploy:** pull the bot files from the Mac (no Gitea needed) — - **Deploy:** the Spark's `~/matrix-bridge` is a Gitea clone tracking `master`, so deploy =
`git fetch origin && git reset --hard origin/master && docker compose up -d --build` (run as
`modelo` from `~/matrix-bridge`). You normally don't run this by hand — the **Update** button on
the Spark Control dashboard (Phase 3) runs exactly this and streams the output: push to Gitea,
then click Update. *(Fallback if Gitea is ever unreachable: scp the files from the Mac —
`scp mac-bridge:/Users/macpro/Projects/matrix-bridge/{Dockerfile,docker-compose.yml,docker-entrypoint.sh,requirements.txt,config.toml,.env} .` `scp mac-bridge:/Users/macpro/Projects/matrix-bridge/{Dockerfile,docker-compose.yml,docker-entrypoint.sh,requirements.txt,config.toml,.env} .`
and `scp -r mac-bridge:/Users/macpro/Projects/matrix-bridge/src .`, then rebuild. and `scp -r mac-bridge:/Users/macpro/Projects/matrix-bridge/src .`, then rebuild.)*
*(Phase 3 switches this to `git pull` once the Spark's `~/matrix-bridge` becomes a Gitea clone —
see `docs/spark-control-integration.md`; not done yet, so scp-from-Mac is still the live path.)*
## Layout ## Layout
- `AGENTS.md` — this file (canonical; `CLAUDE.md` is a relative symlink to it). - `AGENTS.md` — this file (canonical; `CLAUDE.md` is a relative symlink to it).
- `ROADMAP.md` — Phases 14+ with falsifiable exits, plus deferred/future directions. - `ROADMAP.md` — Phases 14+ with falsifiable exits, plus deferred/future directions.
- `README.md` — human-facing intro. - `README.md` — human-facing intro.
- `docs/spark-control-integration.md`Phase 3 spec for the Spark Control dev: the SSH - `docs/spark-control-integration.md`the live Phase 3 command contract: the SSH commands
command contract (status / restart / git-pull update) the dashboard drives, plus the one-time (status / restart / git-pull update / logs) behind the Spark Control tile, plus the now-done
conversion of the Spark's `~/matrix-bridge` to a Gitea clone. matrix-bridge needs no code change. one-time conversion of the Spark's `~/matrix-bridge` to a Gitea clone. matrix-bridge needs no
code change. (Shipped in Spark Control v0.21.0; see Current state.)
- `scripts/launch-claude.sh` — the Mac-side launch wrapper (the only seam that knows the - `scripts/launch-claude.sh` — the Mac-side launch wrapper (the only seam that knows the
Mac's environment). Mac's environment).
- `config.example.toml` — room→repo mapping template; the real `config.toml` is gitignored. - `config.example.toml` — room→repo mapping template; the real `config.toml` is gitignored.
@@ -125,8 +128,11 @@ Condensed from the scoping workshop. Each: the call, why, what it beat.
- **D9 — E2EE deferred (documented tradeoff).** Single-user bot over WireGuard on a private - **D9 — E2EE deferred (documented tradeoff).** Single-user bot over WireGuard on a private
LAN; transport is already private and matrix-nio E2EE adds libolm overhead. *Revisit when:* LAN; transport is already private and matrix-nio E2EE adds libolm overhead. *Revisit when:*
the bot ever handles sensitive content over untrusted transport. the bot ever handles sensitive content over untrusted transport.
- **D10 — Spark Control manages the bot (Phase 3).** Status on the dashboard + one-click - **D10 — Spark Control manages the bot (Phase 3, DONE 2026-06-16).** Status badge + Update /
update/restart, the same SSH-behind-buttons pattern Spark Control uses for the Sparks today. Restart / Stop-Start / Logs buttons on the dashboard, the same SSH-behind-buttons pattern Spark
Control uses for the Sparks. Shipped in Spark Control v0.21.0; connects directly as `modelo` (no
`sudo` wrap — this Spark has no passwordless sudo, so the spec's different-user branch never
applies). Badge reflects container liveness, not Matrix connectivity (see Current state / spec).
- **D11 — Launch into a desktop Terminal, not a headless token (Phase 0).** The SSH session - **D11 — Launch into a desktop Terminal, not a headless token (Phase 0).** The SSH session
can't reach the GUI login Keychain, so a plain `ssh … claude` reports "Not logged in." Rather can't reach the GUI login Keychain, so a plain `ssh … claude` reports "Not logged in." Rather
than mint a long-lived `claude setup-token`, the launcher (`scripts/gui-launch.sh`) uses than mint a long-lived `claude setup-token`, the launcher (`scripts/gui-launch.sh`) uses
@@ -176,6 +182,11 @@ once" is not done.
- **Spark → Mac:** SSH alias `mac-bridge` → the Mac as user `macpro`, dedicated key - **Spark → Mac:** SSH alias `mac-bridge` → the Mac as user `macpro`, dedicated key
(`~/.ssh/id_ed25519` on the Spark, in the Mac's `authorized_keys`). The Spark host's `~/.ssh/config` needs `IdentitiesOnly yes` because a (`~/.ssh/id_ed25519` on the Spark, in the Mac's `authorized_keys`). The Spark host's `~/.ssh/config` needs `IdentitiesOnly yes` because a
`Host *` rule shadows the default key; the container regenerates a clean config from `config.toml [mac]`. `Host *` rule shadows the default key; the container regenerates a clean config from `config.toml [mac]`.
- **Spark → Gitea (deploy/update path):** `~/matrix-bridge` is a git clone tracking `origin/master`
(`ssh://git@immense-voyage.local:59916/grant/matrix-bridge.git`). modelo's `~/.ssh/config` pins the
deploy key for the Gitea host with `IdentitiesOnly yes` — without it git offered the wrong key first
and Gitea returned `Permission denied (publickey)`. **The Spark Control Update button depends on that
ssh-config block; flag it if modelo's account is ever rebuilt.**
- **Mac → Spark:** no authorized key — direct Mac-initiated Spark ops stay owner-run. (This is *not* - **Mac → Spark:** no authorized key — direct Mac-initiated Spark ops stay owner-run. (This is *not*
what Phase 3 closes: Spark Control already has its own SSH channel into `spark-32d0`, so its what Phase 3 closes: Spark Control already has its own SSH channel into `spark-32d0`, so its
status/update/restart buttons ride that, not a Mac→Spark key.) status/update/restart buttons ride that, not a Mac→Spark key.)
@@ -191,18 +202,33 @@ once" is not done.
## Current state ## Current state
- **Live on the Spark (Phases 02 + ask mode).** matrix-nio bot in a Docker container - **Live on the Spark (Phases 03 + ask mode).** matrix-nio bot in a Docker container
(`~/matrix-bridge`, `docker compose up -d --build`): host networking, `restart: unless-stopped`, (`~/matrix-bridge`, now a Gitea clone tracking `master`): host networking, `restart: unless-stopped`,
read-only mounts of `.env`/`config.toml`/SSH key. Runs as `@agent` in 11 project rooms + an read-only mounts of `.env`/`config.toml`/SSH key. Runs as `@agent` in 11 project rooms + an
all-projects fan-out room. Both modes proven — interactive (plain msg → phone via Remote Control) all-projects fan-out room. Both modes proven — interactive (plain msg → phone via Remote Control)
and ask (`?`-prefix → full answer posted back; D12). and ask (`?`-prefix → full answer posted back; D12).
- **Phase 2 — DONE** (owner-confirmed N=3: routes by `room_id`, correct repo, zero wrong-dir launches). - **Phase 2 — DONE** (owner-confirmed N=3: routes by `room_id`, correct repo, zero wrong-dir launches).
- **Phase 3 (Spark Control) — IN PROGRESS, awaiting the Spark Control dev.** The contract is - **Phase 3 (Spark Control) — DONE (2026-06-16), shipped in Spark Control v0.21.0.** matrix-bridge
`docs/spark-control-integration.md`: status/restart/git-pull-update SSH commands + a one-time tile under "Always-on services": live status badge + Update / Restart / Stop-Start / View-logs
conversion of the Spark's `~/matrix-bridge` to a Gitea clone. matrix-bridge needs no code change; buttons, running exactly the spec's commands (`docker inspect` status, `docker restart`, the
remaining work is Spark Control-side (tile + buttons) + the one-time migration. **When live:** trim `git fetch && git reset --hard origin/master && docker compose up -d --build` update streamed live
the spec to the lean command contract, update the Commands "Deploy" entry (scp → git pull), flip with a ~25-min ceiling, `docker logs --tail 100`). All three exit criteria confirmed (status visible
Phase 3 → DONE. + reflects container, update works, restart works). matrix-bridge needed no code change. Deviation:
- **Open / risks:** a `?`-ask in a repo `claude` has never opened may stall on the folder-trust gate connects **directly as `modelo`** (no `sudo -iu` wrap — no passwordless sudo here, so the spec's
— add a trust flag to `ask-claude.sh` if/when hit, not preemptively. different-user branch never applies); tile auto-hides when its SSH-user field is blank or the
container is absent. A LAN-only HTTP API also exists if scripting is ever wanted:
`POST /api/matrix-bridge/update` (+ `/{id}/stream` SSE), `GET /api/matrix-bridge/logs?tail=N`,
status via `GET /api/services`.
- **Open / risks:**
- **Badge = container liveness only, not Matrix connectivity** — a `running` bot disconnected from
Synapse still shows Healthy. Clean fix when "running but silent" bites: a Docker `HEALTHCHECK`
(bot-side liveness signal) so the tile can read `{{.State.Health.Status}}` — a matrix-bridge-side
change; then ping the Spark Control dev to read the health field.
- **Update button depends on modelo's Gitea ssh-config pin** (`IdentitiesOnly yes`, see Infra
facts) — flag it if modelo's account is ever rebuilt.
- A `?`-ask in a repo `claude` has never opened may stall on the folder-trust gate — add a trust
flag to `ask-claude.sh` if/when hit, not preemptively.
- Cosmetic: a fast `docker restart` won't visibly flip the badge red (panel re-checks status only
after the command returns, container already back up); a full `docker stop` turns it red within
~5s. Polling cadence, not a bug.
- **Repo:** `master` == `phase-1`, clean, pushed to Gitea. No test suite (pre-existing). - **Repo:** `master` == `phase-1`, clean, pushed to Gitea. No test suite (pre-existing).
+8 -7
View File
@@ -9,8 +9,9 @@ Runs as a small **matrix-nio** bot in a Docker container on a DGX Spark; a zsh w
Mac (`scripts/launch-claude.sh`) is the only piece that knows the Mac's environment. Routing Mac (`scripts/launch-claude.sh`) is the only piece that knows the Mac's environment. Routing
is deterministic in v1 — the room you message in decides the repo (an explicit config map). is deterministic in v1 — the room you message in decides the repo (an explicit config map).
> Status: **scaffolded, prePhase 0.** No bot code yet. See `AGENTS.md` → `## Current state` > Status: **live on the Spark — Phases 03 + headless "ask" mode shipped.** The bot runs in
> for the active milestone and `ROADMAP.md` for the phase plan. > 11 project rooms + an all-projects room, and is managed from the Spark Control dashboard. See
> `AGENTS.md` → `## Current state` for details and `ROADMAP.md` for the phase plan.
## How it works (v1) ## How it works (v1)
@@ -24,8 +25,8 @@ Matrix message in a project room
## Setup ## Setup
_TODO — filled in as Phase 0 is proven:_ Matrix onboarding (Element + the existing Synapse The bot is live; this is the shape of a fresh install: Matrix onboarding (Element + the existing
homeserver, a bot user), the Mac wrapper, passwordless SSH from the Spark to the Mac, and the Synapse homeserver, a bot user), the Mac wrapper, passwordless SSH from the Spark to the Mac, and
first room→repo mapping. Copy `config.example.toml` to `config.toml` (gitignored) and fill in the room→repo mapping. Copy `config.example.toml` to `config.toml` (gitignored) and fill in real
real room IDs and repo paths. The original scoping docs (SPEC / DECISIONS / KICKOFF) hold the room IDs and repo paths. Deploy and day-2 ops (status / update / restart) run from the Spark
full background. Control dashboard — see `docs/spark-control-integration.md` for the command contract.
+10 -6
View File
@@ -32,17 +32,21 @@ after it.
- **Exit (falsifiable):** 3 real uses across ≥2 rooms, correct repo every time, zero - **Exit (falsifiable):** 3 real uses across ≥2 rooms, correct repo every time, zero
wrong-directory launches. *Met — owner-confirmed N=3 pass.* wrong-directory launches. *Met — owner-confirmed N=3 pass.*
## Phase 3 — Spark Control integration — SPEC DRAFTED (2026-06-15), awaiting Spark Control dev ## Phase 3 — Spark Control integration — DONE (2026-06-16)
- Bot container status surfaced on the Spark Control dashboard. - Bot container status surfaced on the Spark Control dashboard.
- One-click update (pull + restart) wired the same way Spark Control drives the Sparks today - One-click update (pull + restart) wired the same way Spark Control drives the Sparks today
(SSH/commands behind a button). (SSH/commands behind a button).
- **Exit (falsifiable):** bot status is visible and the bot can be updated/restarted from the - **Exit (falsifiable):** bot status is visible and the bot can be updated/restarted from the
panel. panel. *Met — shipped in Spark Control v0.21.0; all three controls confirmed working.*
- **Spec:** `docs/spark-control-integration.md` — the SSH command contract + one-time Spark - **Shipped:** matrix-bridge tile (status badge + Update / Restart / Stop-Start / Logs) running
migration to a Gitea clone. Decided: update = git-pull-from-Gitea; Spark Control's existing the spec's SSH commands; the Spark's `~/matrix-bridge` is now a Gitea clone tracking `master`.
SSH into `spark-32d0` carries the buttons (no new key). matrix-bridge needs no code change; matrix-bridge needed no code change. Deviation: Spark Control connects directly as `modelo` (no
remaining work is Spark Control-side + the one-time migration. `sudo` wrap — no passwordless sudo on this Spark). Live command contract + the Gitea key pin the
Update button depends on: `docs/spark-control-integration.md`.
- **Deferred follow-up:** badge reflects container liveness only, not Matrix connectivity — a
Docker `HEALTHCHECK` (bot-side liveness signal) would let the tile read `{{.State.Health.Status}}`.
matrix-bridge-side change; do it if/when "running but silent" bites.
## Phase 4+ — Future direction (documented, not yet scoped to build) ## Phase 4+ — Future direction (documented, not yet scoped to build)
+60 -107
View File
@@ -1,16 +1,14 @@
# Phase 3 — Spark Control integration (spec for the Spark Control dev) # Phase 3 — Spark Control integration (live command contract)
**Goal (ROADMAP Phase 3):** surface the matrix-bridge bot's container status on the Spark **Status: DONE (2026-06-16), shipped in Spark Control v0.21.0.** The matrix-bridge bot has a
Control dashboard, and add one-click **update** (pull + rebuild + restart) and **restart**, tile on the Spark Control dashboard under "Always-on services" — a live status badge plus
wired the same SSH-behind-buttons way Spark Control already drives the Sparks. **Update**, **Restart**, **Stop/Start**, and **View logs** buttons. All three ROADMAP Phase 3
exit criteria are met (status visible + reflects the container; update works; restart works).
matrix-bridge needed no code change.
**Exit (falsifiable):** bot status is visible on the panel, and the bot can be This document is the **contract**: what each control runs on the Spark, and what the output
updated/restarted from the panel. means. Kept as the reference for what the buttons actually do — and to reproduce by hand if the
dashboard is ever unavailable.
This document is the **contract**: what to run, where, and what the output means. The
matrix-bridge side is fixed below; map the buttons onto Spark Control's existing
managed-service pattern however that codebase already models a Spark/service. No changes to
matrix-bridge are required for this.
--- ---
@@ -21,41 +19,39 @@ A single Docker container on the DGX Spark.
| Fact | Value | | Fact | Value |
|---|---| |---|---|
| Host | `spark-32d0` (`10.59.211.6` on WireGuard), user **`modelo`** | | Host | `spark-32d0` (`10.59.211.6` on WireGuard), user **`modelo`** |
| Project dir | `/home/modelo/matrix-bridge` (`~/matrix-bridge` for modelo) | | Project dir | `/home/modelo/matrix-bridge` — a **Gitea clone tracking `master`** |
| Compose service | `bot` | | Compose service | `bot` |
| Container name | `matrix-bridge` (fixed via `container_name:`) | | Container name | `matrix-bridge` (fixed via `container_name:`) |
| Image | `matrix-bridge-bot` | | Image | `matrix-bridge-bot` |
| Lifecycle | host networking, `restart: unless-stopped` (survives Spark reboot) | | Lifecycle | host networking, `restart: unless-stopped` (survives Spark reboot) |
| Secrets | `.env`, `config.toml`**gitignored**, live only on the Spark, never in git | | Secrets | `.env`, `config.toml`**gitignored**, live only on the Spark, never in git |
Spark Control already SSHes into `spark-32d0`, so these ride the existing channel — **no new Spark Control SSHes into `spark-32d0` as **`modelo`** (the same login it already uses for Spark 2),
key needed.** All commands below assume they run **as `modelo`** (owner of the dir, member of so these ride the existing channel — no new key, and **no `sudo` wrap**: this Spark has no
the `docker` group). If Spark Control's channel connects as a different user, wrap each command passwordless sudo, and since the channel is already `modelo` (owner of the dir, member of the
in `sudo -iu modelo bash -lc '<command>'` — running `git` in modelo's repo as root trips git's `docker` group) every command runs as the right user directly. (The original spec's
"dubious ownership" guard, so don't skip this. `sudo -iu modelo` different-user fallback therefore never applies here.)
Registration on the Spark Control side: the bot's SSH user is a config field (set to `modelo`),
the host reuses the existing Spark 2 connection, and container / dir / branch use the defaults
(`matrix-bridge` / `~/matrix-bridge` / `master`). The tile auto-hides when that user is blank or
the container is absent, so it stays out of the way on installs that don't run the bot.
--- ---
## One-time prerequisites (owner, not Spark Control dev) ## One-time prerequisites — DONE
The bot dir on the Spark was originally populated by `scp` of loose files. To make `~/matrix-bridge` was originally loose files from `scp`; it's now a git clone of the Gitea repo,
git-pull-based updates work it must become a git clone of the Gitea repo **without disturbing converted in place (the gitignored `.env`/`config.toml` were untouched, because `git reset --hard`
the gitignored secrets** (`.env`, `config.toml`). Because those two files are gitignored, ignores them).
`git reset --hard` never touches them — so we can convert the existing dir in place.
**0a. Confirm the Spark can reach + authenticate to Gitea (fail loud here, not at first button press):** **Load-bearing gotcha that's now fixed:** on the Spark, git offered the wrong SSH key first and
Gitea rejected it (`Permission denied (publickey)`) even though the deploy key was correctly
registered. Fixed by pinning it in modelo's `~/.ssh/config` with `IdentitiesOnly yes` for the
Gitea host. **The Update button depends on that block staying in place — flag it if modelo's
account is ever rebuilt.**
```sh The conversion, for reference:
git ls-remote ssh://git@immense-voyage.local:59916/grant/matrix-bridge.git >/dev/null \
&& echo "gitea reachable" || echo "FIX gitea access first"
```
The Spark is on the same LAN as the Start9 host running Gitea, so `immense-voyage.local`
resolves directly — this should just work. If it doesn't, the only likely gap is a key
authorized for read on the Gitea repo available to `modelo` (deploy key or existing key).
Don't proceed until `git ls-remote` succeeds.
**0b. Convert `~/matrix-bridge` to a clone tracking `master` (run as `modelo`):**
```sh ```sh
cd /home/modelo/matrix-bridge cd /home/modelo/matrix-bridge
@@ -66,23 +62,12 @@ git reset --hard origin/master # secrets are gitignored → untouched
git branch --set-upstream-to=origin/master master git branch --set-upstream-to=origin/master master
``` ```
Verify the secrets survived and the container still comes up clean:
```sh
ls -la /home/modelo/matrix-bridge/.env /home/modelo/matrix-bridge/config.toml # both present
git -C /home/modelo/matrix-bridge status # .env/config.toml show as ignored, tree clean
docker compose up -d --build && docker ps --filter name=^/matrix-bridge$
```
`master` is the release branch (today `master == phase-1`). Track whatever you treat as the
release line; the commands below assume `origin/master`.
--- ---
## The contract — commands behind each control ## The contract — commands behind each control
Run from `/home/modelo/matrix-bridge` as `modelo`. Each is idempotent and fail-loud Run from `/home/modelo/matrix-bridge` as `modelo`. Each is idempotent and fail-loud: non-zero
(non-zero exit ⇒ surface it on the panel; don't swallow). exit + stderr is surfaced on the panel, not swallowed.
### Status (poll for the badge) ### Status (poll for the badge)
@@ -90,33 +75,28 @@ Run from `/home/modelo/matrix-bridge` as `modelo`. Each is idempotent and fail-l
docker inspect -f '{{.State.Status}}|{{.State.StartedAt}}|{{.RestartCount}}' matrix-bridge docker inspect -f '{{.State.Status}}|{{.State.StartedAt}}|{{.RestartCount}}' matrix-bridge
``` ```
- Output e.g. `running|2026-06-15T18:02:11.4Z|0`. Parse field 1 for the badge: - `running` → up · `exited` → stopped/crashed · `restarting` → unhealthy/boot-looping ·
- `running`green/up. Field 3 (`RestartCount`) climbing while status flips to non-zero exit (`No such object: matrix-bridge`)**not deployed** (tile hides). A climbing
`restarting`**crash loop** — show it; that's the most useful signal a dashboard gives here. `RestartCount` while status flips to `restarting` is the crash-loop tell.
- `exited` → stopped/crashed. - **Badge = container liveness only, not Matrix connectivity** — a bot that's `running` but
- `restarting` → unhealthy / boot-looping. disconnected from Synapse still shows Healthy. See the HEALTHCHECK note below.
- **Non-zero exit** (`No such object: matrix-bridge`) ⇒ **not deployed** — distinct from - *Cadence note:* a fast `docker restart` won't visibly flip the badge red — the panel re-checks
"stopped". Show that state rather than erroring out. status only after the command returns, by which point the container is already back up. A full
`docker stop` turns it red within ~5s. Polling cadence, not a bug.
Friendlier one-liner for a human-readable badge (empty string when not running): ### Logs
```sh
docker ps --filter name=^/matrix-bridge$ --format '{{.Status}}' # e.g. "Up 2 hours"
```
### Logs (optional "view logs" action — handy for diagnosing a red badge)
```sh ```sh
docker logs --tail 100 matrix-bridge docker logs --tail 100 matrix-bridge
``` ```
### Restart (no code change) ### Restart
```sh ```sh
docker restart matrix-bridge docker restart matrix-bridge
``` ```
### Update (pull latest code + rebuild + recreate) — the headline button ### Update (pull + rebuild + recreate) — the headline button
```sh ```sh
cd /home/modelo/matrix-bridge \ cd /home/modelo/matrix-bridge \
@@ -125,15 +105,11 @@ cd /home/modelo/matrix-bridge \
&& docker compose up -d --build && docker compose up -d --build
``` ```
- `git reset --hard origin/master` is the deploy-box "always match remote" semantic: never gets `git reset --hard origin/master` is the deploy-box "always match remote" semantic: never stuck on
stuck on divergence, and gitignored secrets are preserved. (If you'd rather detect divergence, divergence, and gitignored secrets are preserved. Streamed live on the panel with a ~25-min
`git pull --ff-only` is the gentler alternative — but then a wedged tree needs manual help.) ceiling; non-zero exit + stderr surfaced. **Workflow: push to Gitea, then click Update.**
- `docker compose up -d --build` rebuilds the image and recreates the container only if the
build changed. First build after a base-image bump is slow (minutes); subsequent builds hit
the layer cache. **Treat update as long-running**: stream/await output, set a generous
timeout (≥10 min), and don't block the dashboard on it.
### Stop / Start (optional) ### Stop / Start
```sh ```sh
docker stop matrix-bridge # stop docker stop matrix-bridge # stop
@@ -142,45 +118,22 @@ cd /home/modelo/matrix-bridge && docker compose up -d # start (recreates
--- ---
## Spark Control-side wiring (for the dev) ## Programmatic interface (LAN-only)
Map the above onto however Spark Control already registers a managed Spark/service: The same controls are reachable over HTTP if scripting is ever wanted:
1. **Register `matrix-bridge`** as a managed service (a tile), targeting `spark-32d0` over the - `POST /api/matrix-bridge/update` → returns an id; `GET .../update/{id}` and
existing SSH channel, commands run as `modelo`. `.../update/{id}/stream` (SSE) for progress.
2. **Status badge** ← poll the *Status* command on the panel's normal refresh cadence; map the - `GET /api/matrix-bridge/logs?tail=N`
four states above (running / exited / restarting / not-deployed) to your existing badge - status via `GET /api/services`
vocabulary. Surface `RestartCount` if your tile can show a secondary metric — a climbing
count is the crash-loop tell.
3. **Buttons:** `Update`, `Restart` (required for the exit criterion); `Logs`, `Stop`/`Start`
(optional, nice-to-have).
4. **Fail-loud, surfaced.** Every command's non-zero exit + stderr must reach the panel, not a
silent failure — this mirrors matrix-bridge's own discipline (a bad launch reports back into
the room rather than hanging). Especially: a failed `git fetch` (Gitea unreachable) or a
failed build should show the error, not a stuck spinner.
5. **`Update` is long-running** — see the timeout/streaming note above.
What I deliberately left generic: the tile's exact place in Spark Control's code, its UI, and
its config schema — that's yours to fit to the existing pattern. If a precise drop-in matters,
share how a Spark is currently registered (config entry + the command-runner seam) and I'll
tailor steps 15 to it.
--- ---
## Acceptance (maps to the ROADMAP exit) ## Future enhancement — truer status (not required; matrix-bridge-side)
- [ ] Status tile shows the bot's live state and flips correctly across a manual Status reports container liveness, not Matrix connectivity — the bot can be `running` yet
`docker stop` / `docker start` on the Spark. disconnected from Synapse. A truer signal needs a Docker `HEALTHCHECK` backed by a bot-side
- [ ] `Restart` from the panel cycles the container (status returns to `running`). liveness signal (e.g. the bot touches a file or exposes a tiny endpoint on each successful sync
- [ ] `Update` from the panel pulls a new commit, rebuilds, and recreates the container — and loop), after which Status could read `{{.State.Health.Status}}`. That's a matrix-bridge-side
surfaces a clear error if Gitea is unreachable or the build fails. change — do it if/when "running but silent" actually bites, then tell the Spark Control dev to
read the health field.
---
## Note — optional future enhancement (not required for Phase 3)
The *Status* command reports container liveness (process up), not Matrix connectivity — the bot
can be `running` yet disconnected from Synapse. A truer signal would need a Docker `HEALTHCHECK`
backed by a bot-side liveness signal (e.g. the bot touches a file or exposes a tiny endpoint on
each successful sync loop), after which Status could read `{{.State.Health.Status}}`. That's a
matrix-bridge-side change, out of scope here — flag it if/when "running but silent" actually bites.