Fix Directory topology: containers aren't hosts; bump submodules; docs

bootstrap.js no longer creates host_theta-proxy / host_theta-jump as
synthetic kind:'host' resources. Proxy and jump-host are containers running
on the one real stack host, not machines of their own -- and jump-host
resolves its SSH-reachable-hosts list from exactly kind:'host', so the
mistake wasn't just conceptual, it could offer unreachable SSH targets.
Their services now parent directly onto the stack host, like every other
component. Installs seeded between 2026-08-05 and this release self-heal on
the next ./setup.sh run: existing children are re-parented off the
synthetic hosts and the now-empty synthetic hosts are removed. Validated
live against a running instance carrying the exact bad state.

Bumps submodules to sso-manager-node v1.30.2, proxy v1.35.1,
jump-host v1.19.1.

Also: README's architecture diagram + repo layout were stale (2-service
view predating jump-host/OpenBao, 2 of 5 submodules listed); new
docs/fixtures.md + docs/screenshots.md + bootstrap/seed-demo-users.sh for
consistent, repeatable demo data and screenshot passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113gCdnfSCuZr6xvPDxTo3D
This commit is contained in:
2026-08-06 21:38:48 -04:00
parent ea5d2350a4
commit 5925940936
12 changed files with 606 additions and 66 deletions
+175
View File
@@ -0,0 +1,175 @@
---
title: Canonical demo fixtures
---
# Canonical demo fixtures
The exact users, groups, and hosts that should exist on a stack used for
screenshots or demos, so every future pass seeds the *same* data and a
screenshot diff only shows what actually changed in the UI — not incidental
differences in who/what happened to exist that day.
Persona: a single admin/power-user running theta42 across a **big homelab and
a small business** — mix of self-hosted infra (Proxmox, Pi-hole, Plex) and
office-y apps (invoicing, helpdesk, wiki) with real department structure.
Domain: `laptop-dev.vm42.us` (real public DNS pointing at this machine — see
"Domain" below). Update this doc if the domain ever changes again.
## Users
| uid | Name | Department | Password | Notes |
|---|---|---|---|---|
| `schen` | Sarah Chen | Engineering | `DemoPass123!` | DevOps lead |
| `dkim` | David Kim | Engineering | `DemoPass123!` | Backend developer |
| `ppatel` | Priya Patel | Engineering | `DemoPass123!` | Frontend developer |
| `mjohnson` | Marcus Johnson | Finance | `DemoPass123!` | Finance manager |
| `lnguyen` | Linda Nguyen | Finance | `DemoPass123!` | Bookkeeper |
| `erodriguez` | Emily Rodriguez | Support | `DemoPass123!` | Support lead |
| `tbaker` | Tom Baker | Support | `DemoPass123!` | Support tech |
| `jwilson` | James Wilson | Management | `DemoPass123!` | Owner |
| `svc-monitoring` | — | service account | `ServiceAcct!2024` | Grafana/Prometheus scraping |
| `svc-backup` | — | service account | `ServiceAcct!2024` | Backup automation |
uidNumbers 50005009 in that order. Mail is `<first>.<last>@laptop-dev.vm42.us`
(service accounts use their uid, e.g. `monitoring@laptop-dev.vm42.us`).
## Groups
`groupOfNames`, owned by `cn=admin,...`, member of the department's users:
- `engineering` — schen, dkim, ppatel
- `finance` — mjohnson, lnguyen
- `support` — erodriguez, tbaker
- `management` — jwilson
- `app_sso_service_account` (built-in) — svc-monitoring, svc-backup
## Seeding users + groups
```sh
cd theta-env
docker cp bootstrap/seed-demo-users.sh sso-manager:/tmp/seed-demo-users.sh
docker compose exec -T sso-manager bash /tmp/seed-demo-users.sh
```
Idempotent — re-running skips anything that already exists. If you add a
fixture below, add it to `bootstrap/seed-demo-users.sh` too and keep the two
in sync.
## Proxy hosts
All under `*.laptop-dev.vm42.us`. `setup.sh` itself creates the first two
(sso, proxy) — everything else below is added by hand through Hosts → Add
host (Proxy UI, currently no seed script — see note at the bottom).
| Host | Target | Auth | Notes |
|---|---|---|---|
| `sso` | `sso-manager:3001` | — | created by `setup.sh` |
| `proxy` | `127.0.0.1:3000` | — | created by `setup.sh` |
| `jump` | `jump-host:3002` | — | created by `setup.sh` |
| `proxmox` | `10.0.10.5:8006` (HTTPS) | Basic — realm "Proxmox VE", users `dkim`, `schen` | |
| `pbs` | `10.0.10.6:8007` (HTTPS) | Basic — realm "Proxmox Backup Server", user `dkim` | |
| `grafana` | `10.0.10.12:3000` + LB target `10.0.10.13:3000` | SSO — group `engineering` | load-balancing example |
| `nextcloud` | `10.0.10.20:80` | SSO — any authenticated user | empty allow-lists |
| `ha` | `10.0.10.30:8123` | Basic — realm "Home Assistant", user `jwilson` | |
| `jenkins` | `10.0.10.40:8080` | SSO — group `engineering` | |
| `gitea` | `10.0.10.41:3000` | Off (public) | has its own login |
| `plex` | `10.0.10.50:32400` | Off (public) | has its own login |
| `nas` | `10.0.10.60:5001` (HTTPS) | Basic — realm "Synology NAS", user `jwilson` | |
| `pihole` | `10.0.10.61:80` | Basic — realm "Pi-hole Admin", user `dkim` | |
| `wiki` | `10.0.10.70:3000` | SSO — any authenticated user | |
| `invoices` | `10.0.10.80:8000` | SSO — group `finance` | small-business flavor |
| `helpdesk` | `10.0.10.81:3000` | SSO — group `support` | small-business flavor |
Basic-auth passwords used: `dkim:HomeLab!2024`, `schen:Engineering!24`,
`jwilson:HomeOwner!24`.
## Domain
`CFG_DOMAIN=laptop-dev.vm42.us` in `setup.env`, real public DNS (CNAME
through `718it.biz`) that resolves back to this machine. `CFG_LDAPS_HOST`
is pinned to the LAN IP of the interface holding the default route
(`ip route get 1.1.1.1`), not just any active interface — this machine had
two (wifi + USB ethernet) and only one was actually externally reachable
through the existing port-forward/prod-proxy setup.
A production reverse proxy in front of this host handles TLS/ACME for
`*.718it.biz`-family domains (to avoid hitting Let's Encrypt's rate limits
re-provisioning a cert every time this dev stack rebuilds) — if a fresh
rebuild's Host records don't resolve correctly from the public domain right
after `setup.sh`, that's the layer to check, not this stack's own nginx/lua
routing. `curl -sk -D - https://sso.laptop-dev.vm42.us/` from the host
machine is the fastest way to confirm whether the issue is server-side.
## Known-good login shortcuts
Skip SSO's self-signed-cert dance entirely for admin/screenshot work — every
app ships a local anti-lockout admin account for exactly this:
```sh
# SSO Manager admin (bootstrap account, uid "admin")
node -e "console.log(require('./config/sso-secrets.js').bootstrap.adminPass)"
# Proxy — username proxyadmin2
node -e "console.log(require('./config/proxy-secrets.js').auth.localAdminPass)"
# Jump-host — username jumpadmin
node -e "console.log(require('./config/jump-secrets.js').auth.localAdminPass)"
```
Ports (from `setup.env` — check it, these are operator-configurable):
SSO `3001`, Proxy management UI `3010` (`MGMT_PORT`), Jump-host `3002`.
A freshly-bootstrapped `admin` account hits the onboarding flow (accept ToS,
enter a DOB) before the rest of the UI is usable — expect that on a stack
that was just rebuilt from scratch.
## Jump-host access (SSO Directory resource)
Jump-host's dashboard ("Hosts you can reach") is **not** driven by Proxy's
Host records — it resolves access via the SSO Manager's own Directory
(`kind: host` resources), filtered by the logged-in user's LDAP group
membership. This is a completely separate system from Proxy's HTTP-routing
hosts above; a Proxy host existing does not make it SSH-reachable through
jump-host.
For a `dkim`-can-reach-something screenshot, one Directory host resource was
added:
- **Directory → Add Resource**: name `proxmox-node`, kind `Host`, IP
`10.0.10.5`, parent resource `local (site_local)`.
- **Associated LDAP Groups → `site_local_host_proxmox-node_access`
Members → Add member → `dkim`** (added the individual user directly, not
the `engineering` group — the resource's own auto-generated `_access`
group's member picker only offers individual users).
To reproduce: repeat those two steps for `proxmox-node` if it's missing, or
add more Directory host resources the same way for a richer "Hosts you can
reach" list.
**To screenshot as a real fixture user** (not the `jumpadmin` local
anti-lockout admin, whose "My hosts" list is always non-empty by virtue of
infra ownership, not a real access grant): log out, click "Log in with
Jump" on the login page, and sign in as `dkim` / `DemoPass123!` through the
real SSO flow. This exercises the actual OIDC redirect through
`sso.laptop-dev.vm42.us` — by this point in the session it worked cleanly in
the browser; if it doesn't (stale cookies/redirect loop from an earlier bad
state), see `docs/screenshots.md` §2 for the fallback.
## What's not yet automated
Proxy hosts are still added by hand (no `seed-demo-hosts.sh` equivalent) —
the Proxy UI has no simple LDIF-style bulk-import path the way LDAP does, and
scripting it means either driving the browser or reverse-engineering the
session-cookie login flow for curl. If this list changes often enough to be
annoying, that's the next thing worth building — a small node script run via
`docker compose exec proxy node ...` calling the `Host` model directly,
mirroring how `setup.sh`'s own step 7 registers the sso/proxy hosts.
## Screenshot workflow
See `docs/screenshots.md` for the full screenshot-capture workflow
(save-to-disk, where each doc image lives, the app.modal.js browser-cache
gotcha). Once fixtures match this doc, only re-screenshot pages whose UI
actually changed since the last pass — the data itself shouldn't be the
reason a screenshot looks different.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 KiB

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 332 KiB

+127
View File
@@ -0,0 +1,127 @@
---
title: Updating gitpages screenshots
---
# Updating gitpages screenshots
How to refresh the docs/images/*.png screenshots across sso-manager-node,
proxy, jump-host, and theta-env's own docs. This comes up periodically as the
UI changes — this doc + `docs/fixtures.md` + `bootstrap/seed-demo-users.sh`
exist so it doesn't have to be re-figured-out from scratch each time. Once
fixtures match `docs/fixtures.md`, you only need to re-screenshot pages whose
UI actually changed since the last pass.
## 1. Seed realistic demo data
Screenshots should show a believable homelab/small-business setup, not empty
tables or `test`/`vaulttest` accounts, and the **same** cast every time — see
`docs/fixtures.md` for the canonical list (exact users, groups, hosts,
passwords) and keep it in sync with what's actually seeded. Seed users +
groups with:
```sh
docker cp bootstrap/seed-demo-users.sh sso-manager:/tmp/seed-demo-users.sh
docker compose exec -T sso-manager bash /tmp/seed-demo-users.sh
```
Idempotent — safe to re-run, existing entries are skipped. Proxy hosts have
no equivalent script yet — add them by hand through the Proxy UI (Hosts →
Add host), following `docs/fixtures.md`'s host table exactly (same hostnames,
targets, auth config every time).
## 2. Logging in without fighting SSO/TLS
The SSO's own domain goes through real DNS + a production reverse proxy in
front of this dev stack (see `docs/fixtures.md` → Domain) — logging in via
"Log in with SSO" from Proxy/Jump-host round-trips through that whole path
and can hit stale-cookie/redirect-loop artifacts in an automation browser
profile that a real browser wouldn't. Don't fight this — every app ships a
local anti-lockout admin for exactly this situation. Read the password
straight out of the mounted secrets:
```sh
# SSO Manager admin (bootstrap account, uid "admin")
node -e "console.log(require('./config/sso-secrets.js').bootstrap.adminPass)"
# Proxy — username proxyadmin2
node -e "console.log(require('./config/proxy-secrets.js').auth.localAdminPass)"
# Jump-host — username jumpadmin
node -e "console.log(require('./config/jump-secrets.js').auth.localAdminPass)"
```
Log in at `http://localhost:<port>/login` for each app — plain HTTP on the
mapped port, no cert/cookie issues at all. Ports come from `setup.env`
(operator-configurable) — check it rather than assuming defaults; e.g. this
deployment maps the Proxy UI to `3010` (`MGMT_PORT`), not the usual `3000`.
**Don't touch the login form if it autofills a real saved username/password**
(Chrome profile password manager) — clear the fields and type the local admin
credentials above instead. Never submit a real saved credential on the
user's behalf.
A freshly-bootstrapped `admin` account hits the onboarding flow (accept ToS,
enter a DOB) before the rest of the UI is usable — expect that right after a
from-scratch rebuild.
## 3. Known gotcha: stale `app.modal.js` in the browser cache
If "Add host" (or any `app.modal`-based modal) opens with tabs/fields but no
Save/Cancel footer, check the console for
`TypeError: app.modal.on is not a function`. That means the browser has an
HTTP-cached copy of `@simpleworkjs/frontend/lib/app.modal.js` from before a
method (`on`, `showTab`, etc.) was added — `curl`-ing the same URL returns the
current file, so it's a caching artifact, not a real app bug. Fix it in-page
without a full hard-reload cycle:
```js
// via the browser automation JS tool, in the page context
const res = await fetch('/static-modules/@simpleworkjs/frontend/lib/app.modal.js', {cache: 'reload'});
await res.text(); // {cache:'reload'} both bypasses AND refreshes the cache entry
```
Then reload the page normally — the fresh file sticks for the rest of the
session.
## 4. Capture screenshots
Use `save_to_disk: true` on the browser screenshot action so files land on
disk instead of just being viewed inline. One screenshot per doc image:
| File | Page |
|---|---|
| `sso-manager-node/docs/images/dashboard.png` | SSO Catalog (`/`) |
| `sso-manager-node/docs/images/users.png` | SSO Users → People (`/users`) |
| `sso-manager-node/docs/images/directory.png` | SSO Directory (`/directory`) |
| `sso-manager-node/docs/images/groups.png` | A user's profile → "My Groups" tab |
| `sso-manager-node/docs/images/oauth-clients.png` | Directory → an `oauth` resource → Edit → Details tab |
| `proxy/docs/images/hosts.png` | Proxy Hosts list (`/hosts`) |
| `proxy/docs/images/host-auth-basic.png` | Edit a basic-auth host → Authentication tab |
| `proxy/docs/images/host-auth-sso.png` | Edit an SSO-auth host → Authentication tab |
| `proxy/docs/images/load-balancing.png` | Edit a host with "Additional Targets" filled in → General tab |
| `jump-host/docs/images/login.png` | Jump-host login page |
| `jump-host/docs/images/dashboard.png` | Jump-host dashboard, logged in as a real fixture user (e.g. `dkim` via SSO) with an actual access grant — not the `jumpadmin` local admin, whose host list isn't representative. See `docs/fixtures.md` → Jump-host access. |
| `jump-host/docs/images/sessions.png` | Jump-host active sessions |
| `jump-host/docs/images/audit.png` | Jump-host audit log |
| `theta-env/docs/images/sso-dashboard.png` | same as SSO Catalog above |
| `theta-env/docs/images/proxy-hosts.png` | same as Proxy Hosts above |
| `theta-env/docs/images/jump-dashboard.png` | same as Jump-host dashboard above |
## 5. Where to save them
Only update the **top-level active clones**
`/home/william/dev/theta42/{sso-manager-node,proxy,jump-host,theta-env}` (all
on `master`). The copies nested under `theta-env/sso-manager-node`,
`theta-env/proxy`, `theta-env/jump-host` are git submodules pinned to a
release tag (`HEAD detached at vX.Y.Z`) — those update automatically the next
time theta-env's release/tag-bump workflow rolls the submodule pointer
forward, not by hand-editing the pinned checkout.
```sh
convert screenshot.jpg /home/william/dev/theta42/<repo>/docs/images/<name>.png
```
(`convert` from ImageMagick — the browser tool saves JPEGs, but the repos
track PNGs.)
Commit each repo separately, same as any other change to that component.