Files
computer-playbook/roles/web-app-socialhome/TODO.md
kevinveenbirkenbachandClaude Opus 5 2dbd6eb108 feat(socialhome): implement the role so it actually deploys
The role was a scaffold: tasks/main.yml was a two-line comment, there was no
compose.yml.j2, no domains.yml, no volumes.yml, no env.j2. It deployed nothing
while carrying a Playwright spec and two matrix variants.

It also described the wrong project. meta/main.yml and meta/info.yml pointed at
jaywink/socialhome -- a Django ActivityPub hub. The target here is
social-home-io/socialhome: created 2026-04-21, MPL-2.0, aiohttp + SQLite +
Preact, federating peer-to-peer over Ed25519/WebRTC rather than ActivityPub.
Different codebase, no fork relationship.

It turns out to be the simplest web-app in the repo. One published multi-arch
image (ghcr.io/social-home-io/socialhome:2026.6.16, pinned), no Postgres, no
Redis, no queue, no worker, no collectstatic -- SQLite in one volume, one port,
migrations applied in-process at startup.

Two things the source forced that reading the README would not have:

- [standalone].external_url is reachable only through the TOML file, never
  through SH_* env (config.py:238-261). Without it routes/pairing.py answers
  422 NOT_CONFIGURED and federation pairing is dead, so the role renders a
  config and mounts it as type: config.
- provision_admin() runs from SH_ADMIN_PASSWORD but never calls
  mark_complete(), so / keeps serving the setup wizard instead of the login
  form -- upstream's own docstring claims otherwise. 01_setup.yml closes it
  with an idempotent POST to /api/setup/standalone.

No SSO sidecar, deliberately, against the pgadmin model this otherwise copies:
oauth2-proxy would redirect POST /federation/inbox and /api/pairing/accept to
Keycloak and break the one thing the software exists for.

Verified against the real image rather than reasoned about. The http_status
probe's exact argv ran inside the container and returned 0. /healthz answers
with the byte-for-byte shape the spec asserts. The setup POST returns 201 then
409 and flips setup_required true -> false, which is the whole justification
for 01_setup.yml existing. [standalone].external_url lands in the inbox_url of
POST /api/pairing/initiate. The container runs as uid 10001, matching
meta/users.yml.

Both persona blocks are proven, not assumed: biber gets 401 from the token
endpoint, and the 97 static files contain zero occurrences of logout / sign out
/ abmelden, so utils/logout.js has nothing to click. Injecting a control the
way web-app-prometheus does was rejected -- that model points its button at
OIDC.CLIENT.LOGOUT_URL, which does not exist for a role with no OIDC.

swarm stays disabled for the first round. The orchestrator chains
swarm -> compose -> host under mode_fail_fast, and sequencing measures serial
at 198 jobs, so a red swarm job here would skip 92 unrelated jobs and leave 85
roles untested. Compose-only drops that to 10. Flip it once compose is green.

Adversarial review caught five defects before this landed, three of them
certain rather than likely:

- SOCIALHOME_INTERNAL_URL hardcoded 127.0.0.1, which sys-svc-container rewrites
  to the bridge gateway whenever DOCKER_IN_CONTAINER is set -- from inside the
  Ansible container that address is the Ansible container, so both setup tasks
  would have burned five minutes of retries and failed the play.
- SH_WEBRTC_STUN_URL sat outside the coturn guard while the TURN pair sat
  inside it. Upstream reads by env PRESENCE, so the empty value would have
  beaten the in-app default and left WebRTC with no STUN at all.
- meta/csp.yml whitelisted blob: on worker-src. The bundle registers no service
  worker and constructs no Worker; the only blob of JavaScript is an audio
  worklet, and worklet modules load under script-src. Voice recording would
  have died with the app's own "Audio processing unavailable".
- tasks/main.yml hand-rolled meta: flush_handlers instead of including
  sys-svc-compose's finalize.yml, which silently skipped utils/post_deploy.yml.
- 01_setup.yml was gated on IS_STACK_HOST but not run_once, so a three-manager
  swarm would fire three concurrent setup POSTs at one ingress port.

TODO.md carried a claim that steered the CSP design and was simply false --
that img-src is not a directive csp_filters.py emits. It appends
"img-src * data: blob:" unconditionally at line 383.

lifecycle: alpha puts the role in the CI deploy matrix before it has ever run
anywhere, deliberately: the first CI round is its first execution. README and
TODO say so rather than claiming a maturity it does not have.

Still unverified: no browser has enforced the CSP, the swarm path was only
rendered, TURN against a real coturn is untested, and arm64 has not been
exercised. TODO.md lists them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 17:57:24 +02:00

52 lines
3.0 KiB
Markdown

# Todo
## Never deployed
`lifecycle: alpha` puts this role into the CI deploy matrix before it has ever run against a real
host, deliberately: the first CI round is its first execution. Everything below the first live
`make compose-deploy apps=web-app-socialhome` is design-by-source-reading.
Open items that only a live run can settle:
- `index.html` ships an inline pre-paint theme bootstrap, and the rendered header is
`script-src-elem 'self' https://cdn.…` with no hash, so it is blocked and the first paint
flashes the wrong theme. Pinning `csp.hashes.script-src-elem` fixes it but has to be re-pinned
on every image bump; the flash is the cheaper trade until the theme matters.
- `meta/csp.yml` whitelists `blob:` on `script-src` for the audio worklet the DM composer builds
with `URL.createObjectURL`; worklet modules load under `script-src`, not `worker-src`. The
bundle registers no service worker and constructs no `Worker`. Whether the Preact bundle also
needs `unsafe-inline` for style attributes is unknown until a browser reports violations.
- The published image is multi-arch, but only `linux/amd64` has been exercised.
- `Dockerfile.sh` exposes 8124 for aiolibdatachannel signalling beside 8099. Nothing listens on
it at rest, so only 8099 is published; whether it binds lazily during a call is untested.
## Blocked personas
`PERSONA_ADMINISTRATOR_BLOCKED=true`
: Social Home 2026.6.16 imports `logout` from `client/src/store/auth.ts` in exactly one place —
`client/src/main.tsx` wires it as the 401 handler via `setUnauthorizedHandler(logout)`. No
component renders a logout control, so `personas/utils/logout.js` has nothing to click and the
admin journey cannot complete. The alternative — injecting a `[data-injected-logout]` button via
`templates/javascript.js.j2` — was rejected: the model
(`roles/web-app-prometheus/templates/javascript.js.j2`) points the button at
`OIDC.CLIENT.LOGOUT_URL`, which only exists for OIDC-gated roles, and this role deliberately runs
without SSO. Path back: upstream ships a logout control (or an in-app logout endpoint we can
target), then drop the flag and keep the `adminInteraction` already wired in the spec.
`PERSONA_BIBER_BLOCKED=true`
: The app authenticates against its own `platform_users` table. There is no OIDC client and no
auto-provisioning, so the biber account never exists inside Social Home. Path back: upstream
grows an OIDC/trusted-header auth path.
## Deferred wiring
- `SH_MAX_STORAGE_BYTES` is pinned to 4 GiB in `vars/main.yml` so the app's media cap stays inside
`services.socialhome.min_storage` (5 GB). Raising one without the other lets the volume outgrow
its declared budget.
- `SH_APPS_CATALOG_URL` is emptied so the container never fetches the app catalog from github.com.
Set it to a self-hosted catalog when one exists.
- `SH_CORS_ALLOWED_ORIGINS` is unset — the SPA is same-origin. Needed only if a third-party client
should talk to the API.
- The GFS / global-server side of upstream (`Dockerfile.gfs`) is not deployed. Only the household
instance (`Dockerfile.sh`) is.