da20722e76
gates / gates (push) Successful in 27s
INTERIM CHECKPOINT — evidence off the machine at the end of the phase that produced it (R-320), not at the end of the session. Phases 2-5 follow in a later commit. Phase 0, all three mechanisms proven with their controls: - the fleet floor to 0.261.0 with its declared MinAgent — both demo boxes in 13 s, the hub logging `managed floor SERVED ... from declared (golden 0.258.0)`. - a PRIVATE DRILL CATALOG (admin/app-catalog-drill), so that broken, dummy, cross-repo and engine-major edges can be measured without the live catalog ever carrying one. Positive control quoted, and two negative controls: the live catalog's main and both real boxes' caches unchanged. - a throwaway image store on the scratch guest, which is what makes an UNATTENDED HOLD measurable at all: an edge that PASSES the within-a-major test and still fails. CompareImageRefs was proven to order host:port/ references by RUNNING it (4 positive cases + 1 negative control), not by reading it. Phase 1: real within-a-major upstream edges walked on guest 9202 through the product's own guarded Update, each app seeded and read back through its OWN front door (R-156), with a per-edge verdict record in 09's shape. `inconclusive` is never collapsed into `failed`. TWO INSTRUMENT FIXES, both in this repo's own evidence code: - 00-api-recipe.md said the app page is /app/<n>; it is /apps/<n>, and every call it described 404s. Corrected, with the session-expiry note that cost the same time. - unattended-caller.py's follow() read update_phase/updating off the API ENVELOPE, so both were always None and EVERY followed update ran to its 900 s timeout and was then recorded `timeout` and never-press-again. Fixed before B1 relied on it. R-623. No controller, agent or hub code was written. The live catalog carries no broken reference. Gates: repo_gates.py --fast — all 15 OK, exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
119 lines
5.0 KiB
Markdown
119 lines
5.0 KiB
Markdown
# Driving the guest-9202 controller API from DooPlex — the working recipe (2026-09-21)
|
|
|
|
Controller v0.260.0 in LXC guest **9202 `demo-hp-scratch`** on host `demo-hp`.
|
|
|
|
## The one surprise that saves the most time
|
|
|
|
**Guest 9202 is directly reachable from DooPlex on the home LAN at `192.168.0.114`.**
|
|
You do NOT have to `ssh demo-hp` + `pct exec` to drive the API — that was only needed for
|
|
`docker`/`pct`. Curling straight from DooPlex is what makes a 200 ms poll loop possible at all.
|
|
|
|
Still mandatory: the **`Host: felhom.enkisfelhom.hu`** header (without it every route 404s with the
|
|
public page) and **`-k`** (self-signed cert on traefik). Port 443. `127.0.0.1:8080` inside the guest
|
|
is NOT open — traefik is the only door.
|
|
|
|
## 0. The password, file→file, never through stdout
|
|
|
|
```bash
|
|
python3 /mnt/5_hdd/felhom.eu/git/felhom.eu/scripts/read_credential.py PASSWORD /tmp/.ctlpw
|
|
chmod 600 /tmp/.ctlpw # expect 13 bytes; 15 means it is wearing its quotes
|
|
```
|
|
|
|
## 1. Log in and scrape the session CSRF (both are needed for any POST)
|
|
|
|
curl's cookie jar drops `felhom_session`, so dump the headers and grep it out by hand.
|
|
The CSRF meta tag's closing quote must be stripped or you get a 65-char token that silently
|
|
mismatches — check the length is exactly 64.
|
|
|
|
```bash
|
|
S=/tmp/ctl # any scratch dir
|
|
mkdir -p $S
|
|
B="https://192.168.0.114"
|
|
H='Host: felhom.enkisfelhom.hu'
|
|
PW=$(cat /tmp/.ctlpw)
|
|
|
|
curl -sk -D $S/hdr.txt -o /dev/null -H "$H" -X POST --data-urlencode "password=$PW" "$B/login"
|
|
head -1 $S/hdr.txt # expect: HTTP/2 302
|
|
grep -oiE 'felhom_session=[A-Za-z0-9._-]+' $S/hdr.txt | head -1 > $S/sess.txt # ~79 chars
|
|
|
|
curl -sk -L -H "$H" -H "Cookie: $(cat $S/sess.txt)" "$B/" -o $S/home.html
|
|
grep -oE '<meta name="csrf-token" content="[^"]+"' $S/home.html | head -1 \
|
|
| sed 's/.*content="//;s/"$//' > $S/csrf.txt
|
|
echo "csrf len: $(wc -c < $S/csrf.txt)" # expect 65 = 64 + newline
|
|
```
|
|
|
|
## 2. The call helper — `c.sh GET /api/stacks` / `c.sh POST /api/sync '{...}'`
|
|
|
|
```bash
|
|
cat > $S/c.sh <<'EOF'
|
|
#!/bin/bash
|
|
S=/tmp/ctl
|
|
B="https://192.168.0.114"
|
|
H='Host: felhom.enkisfelhom.hu'
|
|
M=$1; P=$2; D=$3
|
|
SESS=$(cat $S/sess.txt); CT=$(cat $S/csrf.txt)
|
|
if [ "$M" = GET ]; then
|
|
curl -sk -H "$H" -H "Cookie: $SESS" "$B$P"
|
|
else
|
|
curl -sk -H "$H" -H "Cookie: $SESS" -H "X-CSRF-Token: $CT" \
|
|
-H "Content-Type: application/json" -X "$M" ${D:+--data "$D"} "$B$P"
|
|
fi
|
|
EOF
|
|
chmod +x $S/c.sh
|
|
```
|
|
|
|
Worked endpoints (all verified today):
|
|
|
|
| call | meaning |
|
|
|---|---|
|
|
| `c.sh GET /api/stacks` | every stack; `app_config.pinned_images`, `app_config.installed_images`, `template_images`, `catalog_images`, `updating`, `update_phase` |
|
|
| `c.sh GET /api/stacks/vikunja` | one stack, same shape — this is the 200 ms poll target |
|
|
| `c.sh POST /api/stacks/<n>/deploy '{"values":{"DOMAIN":"enkisfelhom.hu","SUBDOMAIN":"tasks"}}'` | real deploy path, answers 202 |
|
|
| `c.sh POST /api/stacks/<n>/update` | the Update button |
|
|
| `c.sh POST /api/sync` | pull the catalog git clone |
|
|
| `c.sh POST /api/stacks/rescan` | **always run this after a sync before reading any badge** (R-607) |
|
|
| `c.sh GET /api/stacks/<n>/logs?lines=200` | app container log |
|
|
|
|
`pinned_images` and `installed_images` live under **`app_config`**, not at the top level — that
|
|
cost a few minutes.
|
|
|
|
## 3. Reading the customer's app page, both languages
|
|
|
|
**The route is `/apps/<name>`, not `/app/<name>`.** This section said `/app/` until 2026-09-21 and
|
|
every call it described 404s. `/` redirects to `/launcher`, and the app links live on `/stacks`.
|
|
|
|
```bash
|
|
curl -sk -H "$H" -H "Cookie: $(cat $S/sess.txt)" "$B/apps/vikunja" # Hungarian
|
|
curl -sk -H "$H" -H "Cookie: $(cat $S/sess.txt)" "$B/apps/vikunja?lang=en" # English
|
|
```
|
|
|
|
**The session dies whenever the controller restarts** — the store is in memory — so a long run must
|
|
be able to log in again mid-flight rather than assuming one login lasts the night.
|
|
|
|
## 4. Shell into the guest (for docker / pct only)
|
|
|
|
```bash
|
|
ssh -o StrictHostKeyChecking=accept-new demo-hp "pct exec 9202 -- bash -c '<cmd>'" 2>/dev/null
|
|
```
|
|
`2>/dev/null` drops the perl locale warnings. For anything with awkward quoting, pipe a script:
|
|
|
|
```bash
|
|
cat <<'EOF' | ssh -o StrictHostKeyChecking=accept-new demo-hp \
|
|
'cat > /tmp/c.sh; pct push 9202 /tmp/c.sh /tmp/c.sh >/dev/null 2>&1; pct exec 9202 -- bash /tmp/c.sh' 2>/dev/null
|
|
docker ps --format '{{.Names}}\t{{.Image}}'
|
|
EOF
|
|
```
|
|
|
|
## 5. Paths inside the running guest
|
|
|
|
- controller data dir (journal, catalog cache):
|
|
`/var/lib/docker/volumes/felhom-controller-data/_data/data/`
|
|
- `update-journal.json` — present only while an update is in flight
|
|
- `catalog-cache/` — a git clone; `git -C … log --oneline -1` tells you what the box actually has
|
|
- stacks: `/opt/docker/stacks/<app>/docker-compose.yml` (the LIVE rendered file) and `app.yaml`
|
|
|
|
## 6. App front doors on 9202 (Host header per app, same IP)
|
|
|
|
`tasks.` vikunja · `status.` uptime-kuma · `wishes.` wishlist · `dashboard.` glance — all
|
|
`…enkisfelhom.hu` against `https://192.168.0.114`.
|