Requirement for a web-app-worldmonitor role: the upstream four-service stack (dashboard, AIS relay, Redis, REST proxy) behind the platform's Keycloak login through oauth2-proxy, in compose and in swarm. It records what the upstream self-hosted mode offers and lacks, eleven proposed decisions the operator has to confirm before implementation, the target role layout and the acceptance criteria. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
15 KiB
037 - World Monitor Role behind the oauth2-proxy SSO Gate
User Story
As a platform administrator of Infinito.Nexus, I want World Monitor integrated as a web-app-worldmonitor role behind the platform's Keycloak SSO (via the oauth2-proxy gate) so that users reach the real-time global intelligence dashboard with their existing Infinito.Nexus identity, in compose and in swarm, without a second login.
Background
World Monitor is an AGPL-3.0 dashboard that aggregates public data feeds (earthquakes, conflicts, markets, vessel tracking and more) on a map. Upstream ships a self-hosted Docker stack of four services.
Upstream facts established by research
Verified against upstream main and release v2.10.0 (2026-09-08).
| Topic | Finding | Source |
|---|---|---|
| Services | worldmonitor (nginx + Node API under supervisord, container port 8080), ais-relay (port 3004), redis (redis:7-alpine, 6379), redis-rest (Upstash-compatible REST proxy, port 80). |
docker-compose.yml |
| Published image | Only ghcr.io/koala73/worldmonitor is published (semver tags, linux/amd64 + linux/arm64, built from docker/Dockerfile). ais-relay (Dockerfile.relay) and redis-rest (docker/Dockerfile.redis-rest) have no published image. |
docker-publish.yml |
| User authentication | None in self-hosted mode. LOCAL_API_MODE=docker has no Clerk or Convex backend, no local user accounts and no OIDC. The app mints an anonymous wms_ browser session signed with WM_SESSION_SECRET. |
SELF_HOSTING.md |
| Mandatory secrets | RELAY_SHARED_SECRET, REDIS_PASSWORD, REDIS_TOKEN, WM_SESSION_SECRET. Each container exits on boot when its secret is missing. |
SELF_HOSTING.md |
| Admin routes | /api/local-* return 403 in Docker mode. Operator settings change only through environment variables plus a container recreate. |
SELF_HOSTING.md |
| Reverse proxy | WM_TRUSTED_PROXY_CIDRS must name the fronting proxy's network, otherwise X-Forwarded-For is ignored and the per-IP LLM cap (50 calls per UTC day) counts the proxy as one client. Invalid values stop the container. |
SELF_HOSTING.md |
| CSP | The bundled nginx emits its own script-src policy without 'unsafe-inline'. |
SELF_HOSTING.md |
| Data seeding | Redis starts empty. Upstream seeds with ./scripts/run-seeders.sh run on the host (Node 22) against the REST proxy published on 127.0.0.1:8079, repeated by host cron. The relay runs additional in-container seed loops when UPSTASH_ALLOW_INSECURE_HTTP=true. |
SELF_HOSTING.md |
| Persistence | One volume, redis-data. All content is re-fetchable cache and seed data. |
docker-compose.yml |
| LLM | Any OpenAI-compatible endpoint through LLM_API_URL (full /v1/chat/completions URL), LLM_API_KEY, LLM_MODEL. |
SELF_HOSTING.md |
| MCP | /api/mcp, authenticated with X-WorldMonitor-Key against WORLDMONITOR_VALID_KEYS. The OAuth path is hosted-only. |
SELF_HOSTING.md |
| Swarm blockers in the upstream file | container_name, depends_on, build:, mem_limit, a host-bound 127.0.0.1:8079 publish. |
docker-compose.yml |
In-repo analogues
- SSO flavor is
oauth2(oauth2-proxy sidecar against Keycloak), as inweb-app-openclawandweb-app-checkmk. The Keycloak client is provisioned by the platform for every role withsso.enabled. - Multi-service compose template with a role-built image:
web-app-moodle. Dockerfile placement andARGwiring follow dockerfile.md. - Shared LiteLLM gateway consumer:
litellmservice flag plussecrets.credentials.litellm_api_keyinweb-app-openclaw. - Generated credentials:
meta/secrets.ymlas inweb-app-openclaw.
Proposed Decisions
The operator MUST confirm or change these before implementation starts. Once confirmed they are not re-litigated during implementation.
| # | Decision | Rationale |
|---|---|---|
| 1 | Role web-app-worldmonitor, canonical hostname worldmonitor.{{ DOMAIN_PRIMARY }}. |
Standard single-host convention. |
| 2 | SSO is the oauth2-proxy gate only. No native OIDC, no user mapping, no in-app accounts. | Upstream self-hosted mode has no user model to map onto. A later native OIDC switch is the existing sso.flavor field, so nothing extra is built for it now. |
| 3 | Every authenticated Keycloak user may enter. No allowed_groups, no meta/rbac.yml. |
The dashboard has no per-user state or admin surface in Docker mode. |
| 4 | The upstream internal mechanisms stay as shipped: the anonymous wms_ session, the relay shared secret, Redis requirepass, the REST proxy bearer token. |
They protect service-to-service traffic and are independent of the user boundary. |
| 5 | worldmonitor uses the published GHCR image pinned to a concrete semver tag. ais-relay and redis-rest are built from the same pinned upstream tag. |
Only one image is published. No fork is maintained. |
| 6 | Redis and the REST proxy run inside the role stack with their own generated credentials, not on the shared svc-db-redis. |
The app speaks only the Upstash REST dialect through the bundled proxy, and upstream runs Redis with allkeys-lru eviction that must not apply to other consumers. |
| 7 | Seeding runs inside the stack as a periodic seeder service. No host Node, no host cron, no published REST proxy port. | The upstream host-side flow needs a host-bound port and a host runtime, which breaks in swarm and violates service isolation. |
| 8 | The Redis volume persists but is excluded from backup. | Content is re-fetchable cache. A restore is a re-seed. |
| 9 | The LLM is wired to the shared LiteLLM gateway when LITELLM_USABLE. OPENROUTER_API_KEY is not used. |
One platform gateway instead of a per-app provider key. |
| 10 | External data-source API keys are optional operator-supplied inventory values, empty when unset. | Upstream degrades gracefully per feed. |
| 11 | /api/mcp is not registered as a platform MCP server in this requirement. It stays behind the gate and WORLDMONITOR_VALID_KEYS stays empty. |
The MCP path authenticates with a static key, not with the platform identity. Tracked separately if wanted. |
Target Schema
Role layout
roles/web-app-worldmonitor/
├── README.md
├── files/Dockerfile
├── files/playwright/{_shared,playwright.spec,test-guest,test-oidc-login,test-isolation}.js
├── meta/{main,info,domains,networks,csp,services,secrets,variants,volumes}.yml
├── tasks/{main,00_core}.yml
├── templates/{compose.yml.j2,env.j2,playwright.env.j2}
└── vars/main.yml
meta/services.yml excerpt
worldmonitor:
modes:
compose: { enabled: true }
swarm: { enabled: true }
image: ghcr.io/koala73/worldmonitor
version: "X.Y.Z"
name: worldmonitor
ports:
internal: { http: 8080 }
local: { http: <free port>, sso: <free port> }
lifecycle: alpha
sso:
bond: 1
enabled: "{{ 'web-app-keycloak' in group_names }}"
shared: "{{ 'web-app-keycloak' in group_names }}"
flavor: oauth2
oauth2:
origin: { host: worldmonitor, port: 8080 }
litellm:
bond: 1
enabled: "{{ LITELLM_USABLE }}"
shared: true
meta/secrets.yml credentials
relay_shared_secret, redis_password, redis_token, session_secret, litellm_api_key.
Acceptance Criteria
Role layout & images
roles/web-app-worldmonitor/exists per the Target Schema and allmeta/*.ymlpass the role-meta lint.worldmonitorrunsghcr.io/koala73/worldmonitorpinned to a concrete semver tag whose existence in GHCR was verified. No:latest.ais-relayandredis-restare built from the same pinned upstream tag, with every external value passed as a DockerARGper dockerfile.md.- Every image used by the role is declared once in
meta/services.ymland is found by the image discovery.
Compose
- A compose deploy brings
worldmonitor,ais-relay,redis,redis-restand the seeder to a steady running state with no manual step.
Swarm
- The same role deploys as a swarm stack and reaches the same steady state.
- The rendered swarm stack contains no
container_name, nomem_limitand no host-bound port publish, and no service fails permanently when its dependencies start after it. - The role-built images are available to every swarm node that may schedule the services.
Secrets
RELAY_SHARED_SECRET,REDIS_PASSWORD,REDIS_TOKENandWM_SESSION_SECRETare generated throughmeta/secrets.ymland no literal secret value is committed.I_UNDERSTAND_THIS_DISABLES_AUTHandALLOW_UNAUTHENTICATED_RELAYare not set anywhere in the role.- Optional external API keys (
FINNHUB_API_KEY,ALPHA_VANTAGE_API_KEY,FRED_API_KEY,EIA_API_KEY,ACLED_*,NASA_FIRMS_API_KEY,AVIATIONSTACK_API,TRAVELPAYOUTS_API_TOKEN,AISSTREAM_API_KEY,CLOUDFLARE_API_TOKEN) reach the containers that read them when the operator sets them, and the stack is healthy when all are empty.
Routing & TLS
https://worldmonitor.{{ DOMAIN_PRIMARY }}/resolves throughsys-svc-proxyto theworldmonitorservice and serves the dashboard SPA.WM_TRUSTED_PROXY_CIDRSis set to the network the fronting proxy reaches the container from, in compose and in swarm, and the app logs the real client address for a proxied request.- The dashboard's live updates (the WebSocket and streaming endpoints it opens) work through the proxy with no console error.
- The canonical surface answers with exactly one
Content-Security-Policyheader, and the dashboard renders under it with no CSP violation in the browser console.
SSO (Decisions #2 to #4)
- With SSO enabled, an unauthenticated request to any path, including
/api/*, is redirected to the Keycloak login. - After login the user lands on the dashboard with no further credential prompt.
- A user with an existing Keycloak session reaches the dashboard without an interactive login.
- After logout through the platform logout, a request to the dashboard is redirected to the Keycloak login again.
- With SSO disabled, the dashboard is reachable anonymously and the stack is otherwise unchanged.
Service isolation
- Only the
worldmonitorHTTP port is published, bound to the platform bind host.redis,redis-restandais-relaypublish no port. - From outside the role's network,
redis:6379,redis-rest:80andais-relay:3004are unreachable. - Redis rejects an unauthenticated command, and
redis-restrejects a request without the bearer token.
Data seeding (Decision #7)
- After a fresh deploy the dashboard shows seeded data from the key-free feeds with no command run on the host.
- Seeding repeats on a fixed interval inside the stack, and one hung feed does not block the others.
Persistence & backup (Decision #8)
- Seeded data survives a recreate of every container in the stack, in compose and on the swarm storage layer.
- The Redis volume is excluded from the platform backup.
LLM (Decision #9)
- When
LITELLM_USABLEis true,LLM_API_URL,LLM_API_KEYandLLM_MODELpoint at the shared LiteLLM gateway with the role's own virtual key, and an intelligence brief request returns generated text. - When it is false, the three variables are empty and the stack is healthy.
Health checks
worldmonitorhas a container healthcheck that fails when either nginx or the Node API is down.redisandredis-resthave container healthchecks, and theredis-restcheck fails when Redis is unreachable.ais-relayhas a container healthcheck.
Variants
meta/variants.ymlcovers SSO on and SSO off, and every variant deploys cleanly on a fresh host.
Playwright
test-guest.jsasserts the redirect to Keycloak for the root path and for an/api/path.test-oidc-login.jslogs in asbiber, asserts the map renders, and ends logged out with the dashboard gated again.test-isolation.jsasserts that no internal service answers on the canonical host.templates/playwright.env.j2exposes every environment variable the specs read, andmake compose-playwright role=web-app-worldmonitorexits 0 with no stub test.- The same specs pass against the swarm deploy.
Documentation & quality
README.mddocuments the gate-only SSO model, the retained internal secrets, the in-stack seeding, the optional API keys with their effect, the image pin and bump policy, and the AGPL-3.0 licence of the upstream.make testis green tree-wide.- This requirement is cross-linked from the implementing PR.
Out of Scope
- Native OIDC, user mapping and in-app roles (no upstream support in self-hosted mode).
- Registering
/api/mcpas a platform MCP server. - The hosted-only features (Clerk entitlements, premium routes, OAuth for MCP).
Validation Apps
make compose-deploy apps=web-app-worldmonitor purge=true full_cycle=true
Cross-mode parity follows the Roundtrip Loop.
Smoke: visit https://worldmonitor.{{ DOMAIN_PRIMARY }}/, pass the Keycloak login, confirm the map shows seeded layers, log out, confirm the gate returns.
Prerequisites
Before implementation, the agent MUST read AGENTS.md, then Compose Loop and the Playwright contract.
Implementation Strategy
Execute autonomously once the Proposed Decisions are confirmed. Scaffold from roles/web-app-openclaw/ (oauth2 flavor, generated credentials, LiteLLM consumer) and take the multi-service compose shape from roles/web-app-moodle/. Before claiming done, verify against the pinned upstream tag: the environment variable names, whether the published app image contains the seeder scripts, the health endpoint path, and the dashboard selectors used by the specs.
Commit Policy
- No git commit until every Acceptance Criterion is checked off.
- When met and
make testis green, instruct the operator to rungit-sign-pushoutside the sandbox. The agent MUST NOT push.
Context
- Upstream repo: https://github.com/koala73/worldmonitor
- Self-hosting guide: https://github.com/koala73/worldmonitor/blob/main/SELF_HOSTING.md
- Closest in-repo analogues:
roles/web-app-openclaw/,029-web-app-checkmk.md