todo.txt-4-trust + safety: create photodna-credentials secret, set PHOTODNA_ENDPOINT + PHOTODNA_API_KEY on pics deployment
todo.txt-5-alertmanager: add cert-manager + CNPG metrics (needs ServiceMonitors in other namespaces)
todo.txt:6:shared bouncer: end-to-end test free user flow (chat.irc.now -> soju-shared)
todo.txt:7:shared bouncer: end-to-end test pro upgrade + dedicated bouncer + gamja sidecar
todo.txt-8-alertmanager: create ops bot, bot-internal-token secret, set ALERT_BOT_ID + ALERT_CHANNEL on bot deployment
/home/josie/development/irc-now/todo.txt-9-
--
/home/josie/development/irc-now/notes/company.md-70-| log search    | $2/mo  | full-text search across scrollback history          |
/home/josie/development/irc-now/notes/company.md-71-| extra storage | $3/mo  | 5GB pack for pastes and images beyond pro limits    |
notes/company.md:72:| dedicated IP  | $3/mo  | avoid per-IP rate limits on shared bouncers         |
/home/josie/development/irc-now/notes/company.md-73-| team billing  | $2/mo  | consolidated invoice for a group, single admin      |
/home/josie/development/irc-now/notes/company.md-74-
--
/home/josie/development/irc-now/notes/company.md-135-- "empowering IRC communities worldwide"
/home/josie/development/irc-now/notes/company.md-136-- "comprehensive bouncer management platform"
notes/company.md:137:- "Get started with our amazing free tier"
/home/josie/development/irc-now/notes/company.md-138-
/home/josie/development/irc-now/notes/company.md-139-## design identity
--
/home/josie/development/irc-now/chat/nginx.conf-11-
/home/josie/development/irc-now/chat/nginx.conf-12-    location /socket {
chat/nginx.conf:13:        proxy_pass http://soju-shared.irc-josie-cloud.svc.cluster.local:8080/;
/home/josie/development/irc-now/chat/nginx.conf-14-        proxy_http_version 1.1;
/home/josie/development/irc-now/chat/nginx.conf-15-        proxy_set_header Host $host;
--
docs/plans/2026-03-03-irc-now-roadmap.md-87-### Phase 7: Shared Bouncer
/home/josie/development/irc-now/docs/plans/2026-03-03-irc-now-roadmap.md-88-
docs/plans/2026-03-03-irc-now-roadmap.md:89:Free-tier shared bouncer, pro-tier dedicated bouncers with gamja sidecar, plan
/home/josie/development/irc-now/docs/plans/2026-03-03-irc-now-roadmap.md-90-gating, validation webhook, bidirectional migration.
/home/josie/development/irc-now/docs/plans/2026-03-03-irc-now-roadmap.md-91-
/home/josie/development/irc-now/docs/plans/2026-03-03-irc-now-roadmap.md-92-| Item | Description | Status |
/home/josie/development/irc-now/docs/plans/2026-03-03-irc-now-roadmap.md-93-|------|-------------|--------|
docs/plans/2026-03-03-irc-now-roadmap.md:94:| soju-shared | SojuBouncer CR for free-tier shared bouncer, multi-user mode | deployed |
docs/plans/2026-03-03-irc-now-roadmap.md:95:| chat.irc.now routing | nginx proxies to soju-shared instead of individual bouncer | deployed |
/home/josie/development/irc-now/docs/plans/2026-03-03-irc-now-roadmap.md-96-| Keycloak chat token | access.token.lifespan set to 8h on chat client (was 5min default) | done |
docs/plans/2026-03-03-irc-now-roadmap.md:97:| Plan gating (web-api) | Free users blocked from creating bouncers + networks, dashboard shows shared bouncer UI | code done |
/home/josie/development/irc-now/docs/plans/2026-03-03-irc-now-roadmap.md-98-| Gamja sidecar | soju-operator adds gamja container + edge Route to dedicated bouncers | code done |
/home/josie/development/irc-now/docs/plans/2026-03-03-irc-now-roadmap.md-99-| bouncer-webhook | ValidatingWebhookConfiguration enforces plan + max bouncer count | code done |
--
/home/josie/development/irc-now/docs/plans/2026-03-03-irc-now-roadmap.md-105-| Downgrade UI | Network picker for reverse migration on pro->free | pending |
/home/josie/development/irc-now/docs/plans/2026-03-03-irc-now-roadmap.md-106-
docs/plans/2026-03-03-irc-now-roadmap.md:107:**Design doc:** `docs/plans/2026-03-07-shared-bouncer-design.md`
docs/plans/2026-03-03-irc-now-roadmap.md:108:**Implementation plan:** `docs/plans/2026-03-07-shared-bouncer-plan.md`
/home/josie/development/irc-now/docs/plans/2026-03-03-irc-now-roadmap.md-109-
/home/josie/development/irc-now/docs/plans/2026-03-03-irc-now-roadmap.md-110----
--
docs/plans/2026-03-03-irc-now-platform-design.md-110-**Billing flow:** Stripe webhooks update Keycloak user attributes (plan tier)
/home/josie/development/irc-now/docs/plans/2026-03-03-irc-now-platform-design.md-111-and trigger operator reconciliation for resource changes (e.g., upgrading from
docs/plans/2026-03-03-irc-now-platform-design.md:112:shared to dedicated bouncer).
/home/josie/development/irc-now/docs/plans/2026-03-03-irc-now-platform-design.md-113-
/home/josie/development/irc-now/docs/plans/2026-03-03-irc-now-platform-design.md-114-### chat.irc.now -- Web Client
--
deploy/instances/soju-shared.yaml-2-kind: SojuBouncer
deploy/instances/soju-shared.yaml-3-metadata:
deploy/instances/soju-shared.yaml:4:  name: soju-shared
deploy/instances/soju-shared.yaml-5-  namespace: irc-josie-cloud
deploy/instances/soju-shared.yaml-6-spec:
deploy/instances/soju-shared.yaml-7-  hostname: irc.now
deploy/instances/soju-shared.yaml:8:  title: irc.now shared bouncer
deploy/instances/soju-shared.yaml-9-  listeners:
deploy/instances/soju-shared.yaml-10-    - address: ":6667"
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-5-## Overview
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-6-
docs/plans/2026-03-07-shared-bouncer-design.md:7:Free-tier users share a single soju bouncer instance (`soju-shared`). Pro-tier users
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-8-get dedicated bouncer instances with a gamja web chat sidecar. A validation webhook
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-9-enforces plan-based quotas. Migration between shared and dedicated is user-initiated
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-20-                    WebSocket                          WebSocket
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-21-                        |                                  |
docs/plans/2026-03-07-shared-bouncer-design.md:22:                 soju-shared:8080                   apple:8080
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-23-              (multi-user bouncer)              (dedicated bouncer)
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-24-                        |                                  |
docs/plans/2026-03-07-shared-bouncer-design.md:25:                 soju_shared DB                     soju_apple DB
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-26-              (all free user data)              (single user data)
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-27-```
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-28-
docs/plans/2026-03-07-shared-bouncer-design.md:29:### soju-shared
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-30-
docs/plans/2026-03-07-shared-bouncer-design.md:31:A SojuBouncer CR deployed via `deploy/instances/soju-shared.yaml`. Serves all free-tier
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-32-users with soju's built-in multi-user isolation (`enable-user-on-auth true`). Each user
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-33-authenticates via SASL OAUTHBEARER and gets their own networks, channels, and message
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-44-### chat.irc.now
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-45-
docs/plans/2026-03-07-shared-bouncer-design.md:46:Unchanged. gamja + nginx proxying WebSocket to `soju-shared:8080`. Free-tier web chat
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-47-entry point.
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-48-
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-62-### Portal level (web-api)
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-63-
docs/plans/2026-03-07-shared-bouncer-design.md:64:- `POST /bouncers`: rejected if `plan == "free"`. Free users use soju-shared
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-65-  automatically.
docs/plans/2026-03-07-shared-bouncer-design.md-66-- Dashboard UI: free users see "Shared Bouncer" section with a link to chat.irc.now
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-87-Free-tier network limit (1 upstream network) enforced at both the portal level and
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-88-via soju configuration where possible. The portal talks to soju's admin socket to
docs/plans/2026-03-07-shared-bouncer-design.md:89:manage networks for shared bouncer users.
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-90-
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-91-## Gamja Sidecar (Dedicated Bouncers)
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-92-
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-93-When the soju-operator reconciles a dedicated bouncer (any SojuBouncer CR that is
docs/plans/2026-03-07-shared-bouncer-design.md:94:not `soju-shared`), it adds:
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-95-
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-96-### Sidecar container
--
docs/plans/2026-03-07-shared-bouncer-design.md-120-3. If unchecked: dedicated bouncer created fresh.
docs/plans/2026-03-07-shared-bouncer-design.md-121-4. If checked: dedicated bouncer created, then a Kubernetes Job migrates user data
docs/plans/2026-03-07-shared-bouncer-design.md:122:   from `soju_shared` DB to the new tenant DB.
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-123-
docs/plans/2026-03-07-shared-bouncer-design.md-124-Migration Job copies:
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-131-Message logs are NOT migrated (scrollback resets).
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-132-
docs/plans/2026-03-07-shared-bouncer-design.md:133:The Job removes the user from `soju_shared` DB after successful copy. If the Job
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-134-fails, the dedicated bouncer CR is deleted and the user stays on shared. Portal shows
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-135-an error.
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-140-
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-141-1. Portal shows "Migrate to Shared Bouncer" with a network picker if the user has
docs/plans/2026-03-07-shared-bouncer-design.md:142:   more than 1 upstream network (free tier allows 1).
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-143-2. Reverse migration Job copies selected network + channels + receipts back to
docs/plans/2026-03-07-shared-bouncer-design.md:144:   `soju_shared` DB, within free-tier quota limits.
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-145-3. Excess networks and their data are dropped.
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-146-4. Message logs not migrated (48h scrollback resets).
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-149-## Components to Build
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-150-
docs/plans/2026-03-07-shared-bouncer-design.md:151:1. **Deploy soju-shared** -- apply `deploy/instances/soju-shared.yaml`, verify
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-152-   chat.irc.now connects to it
docs/plans/2026-03-07-shared-bouncer-design.md-153-2. **Soju-operator: gamja sidecar** -- add gamja container + edge Route generation
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-154-   for dedicated bouncers
docs/plans/2026-03-07-shared-bouncer-design.md:155:3. **Web-api: plan gating** -- free users see shared bouncer UI, pro users see
docs/plans/2026-03-07-shared-bouncer-design.md:156:   bouncer management. Block bouncer creation for free tier.
docs/plans/2026-03-07-shared-bouncer-design.md-157-4. **Web-api: migration UI** -- "Migrate from shared" checkbox on create,
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-158-   "Migrate to shared" on delete/downgrade with network picker
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-161-6. **Validation webhook (bouncer-webhook)** -- new crate, ValidatingWebhookConfiguration.
docs/plans/2026-03-07-shared-bouncer-design.md-162-   Enforces: no bouncer creation for free, max bouncer count for pro, network limits.
docs/plans/2026-03-07-shared-bouncer-design.md:163:7. **Network limit enforcement** -- portal + soju-level checks for free-tier
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-design.md-164-   1-network limit
--
docs/plans/2026-03-06-alertmanager-design.md-19-Severity: critical.
/home/josie/development/irc-now/docs/plans/2026-03-06-alertmanager-design.md-20-
docs/plans/2026-03-06-alertmanager-design.md:21:Services: web-api, txt, pics, bot, soju (shared bouncer), ergo (irc-now-net),
/home/josie/development/irc-now/docs/plans/2026-03-06-alertmanager-design.md-22-chat, keycloak.
/home/josie/development/irc-now/docs/plans/2026-03-06-alertmanager-design.md-23-
--
docs/plans/2026-03-07-shared-bouncer-plan.md-3-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-4-
docs/plans/2026-03-07-shared-bouncer-plan.md:5:**Goal:** Implement free-tier shared bouncer, pro-tier dedicated bouncers with gamja sidecar, plan gating, validation webhook, and bidirectional migration.
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-6-
docs/plans/2026-03-07-shared-bouncer-plan.md:7:**Architecture:** soju-shared serves all free users via multi-user mode. Pro users get dedicated SojuBouncer CRs with a gamja sidecar container and edge Route. A ValidatingWebhookConfiguration enforces plan-based quotas. Migration Jobs copy user data between tenant DBs.
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-8-
docs/plans/2026-03-07-shared-bouncer-plan.md-9-**Tech Stack:** Rust (Axum 0.8, kube.rs 3.0, sqlx, tokio-postgres), Kubernetes (OCP 4.20, cert-manager), gamja (nginx), Keycloak 26.1
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-10-
docs/plans/2026-03-07-shared-bouncer-plan.md:11:**Design doc:** `docs/plans/2026-03-07-shared-bouncer-design.md`
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-12-
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-13----
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-14-
docs/plans/2026-03-07-shared-bouncer-plan.md:15:## Task 1: Deploy soju-shared
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-16-
docs/plans/2026-03-07-shared-bouncer-plan.md:17:Deploy the shared bouncer instance and verify chat.irc.now connects.
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-18-
docs/plans/2026-03-07-shared-bouncer-plan.md-19-**Files:**
docs/plans/2026-03-07-shared-bouncer-plan.md:20:- Modify: `deploy/instances/soju-shared.yaml`
docs/plans/2026-03-07-shared-bouncer-plan.md-21-- Modify: `chat/nginx.conf`
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-22-
docs/plans/2026-03-07-shared-bouncer-plan.md:23:**Step 1: Apply the soju-shared CR**
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-24-
docs/plans/2026-03-07-shared-bouncer-plan.md:25:The manifest already exists at `deploy/instances/soju-shared.yaml`. It needs the `auth`
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-26-field format to match what the operator expects (`oauth2_url` not `oauth2Url`). Verify
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-27-the YAML matches the operator's `AuthSpec` struct. The operator will create the
--
docs/plans/2026-03-07-shared-bouncer-plan.md-30-Run:
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-31-```bash
docs/plans/2026-03-07-shared-bouncer-plan.md:32:oc apply -f deploy/instances/soju-shared.yaml -n irc-josie-cloud
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-33-```
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-34-
docs/plans/2026-03-07-shared-bouncer-plan.md:35:Expected: SojuBouncer `soju-shared` created.
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-36-
docs/plans/2026-03-07-shared-bouncer-plan.md-37-**Step 2: Verify resources created**
--
docs/plans/2026-03-07-shared-bouncer-plan.md-39-Run:
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-40-```bash
docs/plans/2026-03-07-shared-bouncer-plan.md:41:oc get deployment soju-shared -n irc-josie-cloud
docs/plans/2026-03-07-shared-bouncer-plan.md:42:oc get svc soju-shared -n irc-josie-cloud
docs/plans/2026-03-07-shared-bouncer-plan.md:43:oc get cm soju-shared-config -n irc-josie-cloud
docs/plans/2026-03-07-shared-bouncer-plan.md:44:oc get secret soju-shared-db -n irc-josie-cloud
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-45-```
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-46-
--
docs/plans/2026-03-07-shared-bouncer-plan.md-49-**Step 3: Update chat nginx.conf**
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-50-
docs/plans/2026-03-07-shared-bouncer-plan.md:51:The repo copy of `chat/nginx.conf` currently points to `soju-shared` which is correct.
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-52-The running container was manually patched to `josie`. Rebuild chat to pick up the
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-53-repo config.
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-58-```
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-59-
docs/plans/2026-03-07-shared-bouncer-plan.md:60:Expected: Build succeeds, chat pod restarts with nginx proxying to `soju-shared:8080`.
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-61-
docs/plans/2026-03-07-shared-bouncer-plan.md-62-**Step 4: Verify chat.irc.now connects**
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-63-
docs/plans/2026-03-07-shared-bouncer-plan.md:64:Open `https://chat.irc.now`, authenticate via Keycloak. Verify connection to soju-shared
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-65-and that messages work.
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-66-
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-70-
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-71-```bash
docs/plans/2026-03-07-shared-bouncer-plan.md:72:git add deploy/instances/soju-shared.yaml chat/nginx.conf
docs/plans/2026-03-07-shared-bouncer-plan.md:73:git commit -m "deploy: apply soju-shared bouncer for free tier"
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-74-```
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-75-
--
docs/plans/2026-03-07-shared-bouncer-plan.md-122-For free users (`plan != "pro"`), show:
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-123-```html
docs/plans/2026-03-07-shared-bouncer-plan.md:124:<h3>shared bouncer</h3>
docs/plans/2026-03-07-shared-bouncer-plan.md:125:<p>you're on the shared bouncer. <a href="https://chat.irc.now">open web chat</a></p>
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-126-<a href="/billing" class="btn-fill">upgrade to pro</a>
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-127-```
--
docs/plans/2026-03-07-shared-bouncer-plan.md-131-**Step 6: Update bouncers page**
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-132-
docs/plans/2026-03-07-shared-bouncer-plan.md:133:In `crates/web-api/templates/bouncers.html`, add a "migrate from shared bouncer"
docs/plans/2026-03-07-shared-bouncer-plan.md-134-checkbox to the create form (lines 13-24):
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-135-
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-138-  <label>
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-139-    <input type="checkbox" name="migrate" value="true">
docs/plans/2026-03-07-shared-bouncer-plan.md:140:    migrate data from shared bouncer
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-141-  </label>
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-142-</div>
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-172-
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-173-Test that `build_deployment()` adds a second container when the bouncer name is NOT
docs/plans/2026-03-07-shared-bouncer-plan.md:174:`soju-shared`. Test that `soju-shared` gets only the soju container.
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-175-
docs/plans/2026-03-07-shared-bouncer-plan.md-176-**Step 2: Run test to verify it fails**
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-229-In `build_configmap()` at `crates/soju-operator/src/resources/configmap.rs`, add
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-230-gamja nginx config and config.json as additional data keys when the bouncer name
docs/plans/2026-03-07-shared-bouncer-plan.md:231:is not `soju-shared`:
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-232-
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-233-```rust
docs/plans/2026-03-07-shared-bouncer-plan.md:234:if bouncer.name_any() != "soju-shared" {
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-235-    data.insert("gamja-nginx".to_string(), gamja_nginx_config());
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-236-    data.insert("gamja-config".to_string(), gamja_config_json(bouncer));
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-242-In `build_deployment()` at `crates/soju-operator/src/resources/deployment.rs`, after
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-243-the soju container (line ~163), add a gamja sidecar container when the bouncer name
docs/plans/2026-03-07-shared-bouncer-plan.md:244:is not `soju-shared`:
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-245-
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-246-```rust
docs/plans/2026-03-07-shared-bouncer-plan.md:247:if bouncer.name_any() != "soju-shared" {
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-248-    containers.push(Container {
docs/plans/2026-03-07-shared-bouncer-plan.md-249-        name: "gamja".to_string(),
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-302-
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-303-```rust
docs/plans/2026-03-07-shared-bouncer-plan.md:304:if bouncer.name_any() != "soju-shared" {
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-305-    let edge_route = build_edge_route(&bouncer);
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-306-    apply_resource(&api_route, &edge_route).await?;
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-327-oc get pods -l app.kubernetes.io/managed-by=soju-operator -n irc-josie-cloud
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-328-# Each dedicated bouncer pod should show 2/2 containers
docs/plans/2026-03-07-shared-bouncer-plan.md:329:# soju-shared should show 1/1
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-330-```
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-331-
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-380-
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-381-1. Extracts `irc.now/owner` label from the incoming CR
docs/plans/2026-03-07-shared-bouncer-plan.md:382:2. If owner is empty (admin-applied CR like soju-shared), ALLOW
docs/plans/2026-03-07-shared-bouncer-plan.md-383-3. Queries the accounts DB: `SELECT plan FROM users WHERE keycloak_sub = $1`
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-384-4. If plan != 'pro', DENY with message "upgrade to pro to create a dedicated bouncer"
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-511-
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-512-/// Copy a user's data from dedicated DB back to shared DB,
docs/plans/2026-03-07-shared-bouncer-plan.md:513:/// keeping only the specified network (free tier = 1 network).
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-514-pub async fn reverse_migrate_user(
docs/plans/2026-03-07-shared-bouncer-plan.md-515-    source: &PgPool,
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-527-
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-528-The exact table names and schemas come from soju's Postgres schema. Query the
docs/plans/2026-03-07-shared-bouncer-plan.md:529:soju-shared DB to discover the schema:
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-530-
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-531-```bash
docs/plans/2026-03-07-shared-bouncer-plan.md:532:oc exec soju-db-1 -n irc-josie-cloud -- psql -U accounts -d soju_shared -c '\dt'
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-533-```
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-534-
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-557-
docs/plans/2026-03-07-shared-bouncer-plan.md-558-After the bouncer CR is created and becomes Ready, if `migrate == Some("true")`:
docs/plans/2026-03-07-shared-bouncer-plan.md:559:1. Connect to the soju-shared tenant DB
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-560-2. Connect to the new bouncer's tenant DB
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-561-3. Call `migrate_user(shared_pool, new_pool, &user.email)`
--
docs/plans/2026-03-07-shared-bouncer-plan.md-581-## Task 6: Network limit enforcement
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-582-
docs/plans/2026-03-07-shared-bouncer-plan.md:583:Enforce 1 upstream network limit for free-tier users on the shared bouncer.
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-584-
docs/plans/2026-03-07-shared-bouncer-plan.md-585-**Files:**
--
docs/plans/2026-03-07-shared-bouncer-plan.md-589-**Step 1: Write test for network limit**
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-590-
docs/plans/2026-03-07-shared-bouncer-plan.md:591:Test that a free user on the shared bouncer cannot add a second upstream network.
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-592-
docs/plans/2026-03-07-shared-bouncer-plan.md-593-**Step 2: Run test to verify it fails**
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-601-current count. If already at 1, reject with a message to upgrade.
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-602-
docs/plans/2026-03-07-shared-bouncer-plan.md:603:The portal manages networks for shared bouncer users by talking to soju's admin
docs/plans/2026-03-07-shared-bouncer-plan.md:604:socket or directly querying the shared bouncer's tenant DB:
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-605-
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-606-```sql
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-610-```
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-611-
docs/plans/2026-03-07-shared-bouncer-plan.md:612:If count >= 1, return 403 with "free tier is limited to 1 upstream network".
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-613-
docs/plans/2026-03-07-shared-bouncer-plan.md-614-**Step 4: Run test to verify it passes**
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-626-```bash
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-627-git add crates/web-api/
docs/plans/2026-03-07-shared-bouncer-plan.md:628:git commit -m "feat(web-api): enforce 1-network limit for free tier"
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-629-```
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-630-
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-654-
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-655-1. Log in as a free user at my.irc.now
docs/plans/2026-03-07-shared-bouncer-plan.md:656:2. Dashboard shows "shared bouncer" section with chat.irc.now link
docs/plans/2026-03-07-shared-bouncer-plan.md:657:3. Navigate to /bouncers -- no create button, shows shared bouncer info
docs/plans/2026-03-07-shared-bouncer-plan.md:658:4. Open chat.irc.now -- connects to soju-shared, can join channels
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-659-5. Try to apply a SojuBouncer CR with the free user's owner label -- webhook denies
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-660-
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-672-1. Cancel subscription (or manually set plan='free')
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-673-2. Delete dedicated bouncer, select network to keep
docs/plans/2026-03-07-shared-bouncer-plan.md:674:3. Verify reverse migration to shared bouncer
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-675-4. Verify dedicated bouncer CR and resources cleaned up
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-676-5. Verify user can connect to chat.irc.now again
--
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-680-```bash
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-681-git add -A
docs/plans/2026-03-07-shared-bouncer-plan.md:682:git commit -m "fix: integration test fixes for shared bouncer"
/home/josie/development/irc-now/docs/plans/2026-03-07-shared-bouncer-plan.md-683-```
--
/home/josie/development/irc-now/crates/web-api/templates/bouncer_detail.html-58-    <h2>danger zone</h2>
/home/josie/development/irc-now/crates/web-api/templates/bouncer_detail.html-59-    {% if !networks.is_empty() %}
crates/web-api/templates/bouncer_detail.html:60:    <p style="margin-bottom: 12px;">move to the shared bouncer, keeping one network.</p>
crates/web-api/templates/bouncer_detail.html-61-    <a href="/bouncers/{{ name }}/downgrade" class="btn-fill" style="background: #AA5555; text-decoration: none; display: inline-block; margin-bottom: 16px;">
/home/josie/development/irc-now/crates/web-api/templates/bouncer_detail.html-62-      downgrade to shared
--
crates/web-api/templates/bouncer_downgrade.html-32-      <button type="submit" class="btn-fill" style="background: #AA5555; margin-top: 16px;"
/home/josie/development/irc-now/crates/web-api/templates/bouncer_downgrade.html-33-              onclick="return confirm('this will delete your dedicated bouncer and move to the shared tier. this cannot be undone.');">
crates/web-api/templates/bouncer_downgrade.html:34:        downgrade to shared bouncer
/home/josie/development/irc-now/crates/web-api/templates/bouncer_downgrade.html-35-      </button>
/home/josie/development/irc-now/crates/web-api/templates/bouncer_downgrade.html-36-    </form>
--
/home/josie/development/irc-now/crates/web-api/templates/bouncers.html-20-        <label>
/home/josie/development/irc-now/crates/web-api/templates/bouncers.html-21-          <input type="checkbox" name="migrate" value="true">
crates/web-api/templates/bouncers.html:22:          migrate data from shared bouncer
/home/josie/development/irc-now/crates/web-api/templates/bouncers.html-23-        </label>
/home/josie/development/irc-now/crates/web-api/templates/bouncers.html-24-      </div>
--
/home/josie/development/irc-now/crates/web-api/templates/dashboard.html-75-    {% endif %}
/home/josie/development/irc-now/crates/web-api/templates/dashboard.html-76-    {% else %}
crates/web-api/templates/dashboard.html:77:    <h3>shared bouncer</h3>
crates/web-api/templates/dashboard.html:78:    <p>your free plan includes access to the shared bouncer via web chat.</p>
/home/josie/development/irc-now/crates/web-api/templates/dashboard.html-79-    <div class="card-actions">
crates/web-api/templates/dashboard.html-80-      <a href="https://chat.irc.now" class="btn-fill">chat.irc.now</a>
--
/home/josie/development/irc-now/crates/soju-operator/src/resources/deployment.rs-141-    let mut containers = vec![container];
/home/josie/development/irc-now/crates/soju-operator/src/resources/deployment.rs-142-
crates/soju-operator/src/resources/deployment.rs:143:    if name != "soju-shared" {
/home/josie/development/irc-now/crates/soju-operator/src/resources/deployment.rs-144-        containers.push(Container {
crates/soju-operator/src/resources/deployment.rs-145-            name: "gamja".to_string(),
--
crates/soju-operator/src/resources/configmap.rs-15-    let mut data = std::collections::BTreeMap::from([("config".to_string(), config)]);
/home/josie/development/irc-now/crates/soju-operator/src/resources/configmap.rs-16-
crates/soju-operator/src/resources/configmap.rs:17:    if name != "soju-shared" {
crates/soju-operator/src/resources/configmap.rs-18-        data.insert("gamja-nginx".to_string(), gamja::gamja_nginx_config());
/home/josie/development/irc-now/crates/soju-operator/src/resources/configmap.rs-19-        data.insert(
--
/home/josie/development/irc-now/crates/soju-operator/src/resources/service.rs-33-        .collect();
/home/josie/development/irc-now/crates/soju-operator/src/resources/service.rs-34-
crates/soju-operator/src/resources/service.rs:35:    if name != "soju-shared" {
/home/josie/development/irc-now/crates/soju-operator/src/resources/service.rs-36-        ports.push(ServicePort {
crates/soju-operator/src/resources/service.rs-37-            name: Some("http".to_string()),
--
/home/josie/development/irc-now/crates/soju-operator/src/resources/service.rs-87-
/home/josie/development/irc-now/crates/soju-operator/src/resources/service.rs-88-    #[test]
crates/soju-operator/src/resources/service.rs:89:    fn shared_bouncer_has_no_http_port() {
/home/josie/development/irc-now/crates/soju-operator/src/resources/service.rs-90-        let mut b = test_bouncer();
crates/soju-operator/src/resources/service.rs:91:        b.metadata.name = Some("soju-shared".to_string());
/home/josie/development/irc-now/crates/soju-operator/src/resources/service.rs-92-        let svc = build_service(&b);
/home/josie/development/irc-now/crates/soju-operator/src/resources/service.rs-93-        let ports = svc.spec.unwrap().ports.unwrap();
--
/home/josie/development/irc-now/crates/soju-operator/src/resources/service.rs-99-    fn service_with_no_listeners_shared_has_no_ports() {
/home/josie/development/irc-now/crates/soju-operator/src/resources/service.rs-100-        let mut b = test_bouncer();
crates/soju-operator/src/resources/service.rs:101:        b.metadata.name = Some("soju-shared".to_string());
/home/josie/development/irc-now/crates/soju-operator/src/resources/service.rs-102-        b.spec.listeners.clear();
/home/josie/development/irc-now/crates/soju-operator/src/resources/service.rs-103-        let svc = build_service(&b);
--
/home/josie/development/irc-now/crates/soju-operator/src/controller.rs-194-    }
/home/josie/development/irc-now/crates/soju-operator/src/controller.rs-195-
crates/soju-operator/src/controller.rs:196:    if name != "soju-shared" {
/home/josie/development/irc-now/crates/soju-operator/src/controller.rs-197-        let edge_route = build_edge_route(bouncer);
crates/soju-operator/src/controller.rs-198-        let gvk = GroupVersionKind::gvk("route.openshift.io", "v1", "Route");
--
/home/josie/development/irc-now/crates/web-api/src/main.rs-36-}
/home/josie/development/irc-now/crates/web-api/src/main.rs-37-
crates/web-api/src/main.rs:38:async fn sync_shared_bouncer_limits(kube: kube::Client, namespace: String) {
crates/web-api/src/main.rs-39-    let mut interval = tokio::time::interval(std::time::Duration::from_secs(300));
/home/josie/development/irc-now/crates/web-api/src/main.rs-40-    loop {
/home/josie/development/irc-now/crates/web-api/src/main.rs-41-        interval.tick().await;
crates/web-api/src/main.rs:42:        match soju_migrate::connect_soju_db(&kube, &namespace, "soju-shared-db").await {
/home/josie/development/irc-now/crates/web-api/src/main.rs-43-            Ok(client) => {
/home/josie/development/irc-now/crates/web-api/src/main.rs-44-                match client
--
/home/josie/development/irc-now/crates/web-api/src/main.rs-51-                    Ok(n) => {
/home/josie/development/irc-now/crates/web-api/src/main.rs-52-                        if n > 0 {
crates/web-api/src/main.rs:53:                            tracing::info!("synced max_networks=1 for {n} shared bouncer users");
/home/josie/development/irc-now/crates/web-api/src/main.rs-54-                        }
/home/josie/development/irc-now/crates/web-api/src/main.rs-55-                    }
crates/web-api/src/main.rs:56:                    Err(e) => tracing::warn!("shared bouncer max_networks sync failed: {e}"),
/home/josie/development/irc-now/crates/web-api/src/main.rs-57-                }
/home/josie/development/irc-now/crates/web-api/src/main.rs-58-            }
crates/web-api/src/main.rs:59:            Err(e) => tracing::debug!("shared bouncer db not available: {e}"),
/home/josie/development/irc-now/crates/web-api/src/main.rs-60-        }
/home/josie/development/irc-now/crates/web-api/src/main.rs-61-    }
--
/home/josie/development/irc-now/crates/web-api/src/main.rs-202-
crates/web-api/src/main.rs-203-    tokio::spawn(bouncer_watcher::watch_bouncers(state.clone()));
crates/web-api/src/main.rs:204:    tokio::spawn(sync_shared_bouncer_limits(state.kube.clone(), state.namespace.clone()));
crates/web-api/src/main.rs-205-    tokio::spawn(business_metrics::record_event_metrics(state.db.clone()));
crates/web-api/src/main.rs-206-    tokio::spawn(business_metrics::record_soju_metrics(state.kube.clone(), state.namespace.clone()));
--
crates/web-api/src/bouncer_watcher.rs-186-async fn run_migration(state: &AppState, bouncer_name: &str, username: &str) -> Result<(), String> {
crates/web-api/src/bouncer_watcher.rs-187-    let source = crate::soju_migrate::connect_soju_db(
crates/web-api/src/bouncer_watcher.rs:188:        &state.kube, &state.namespace, "soju-shared-db",
/home/josie/development/irc-now/crates/web-api/src/bouncer_watcher.rs-189-    ).await?;
/home/josie/development/irc-now/crates/web-api/src/bouncer_watcher.rs-190-
--
/home/josie/development/irc-now/crates/bot/templates/bot_create.html-14-
/home/josie/development/irc-now/crates/bot/templates/bot_create.html-15-    <label>network address</label>
crates/bot/templates/bot_create.html:16:    <input type="text" name="network_addr" value="irc+insecure://soju-shared:6667" class="input" required>
/home/josie/development/irc-now/crates/bot/templates/bot_create.html-17-
/home/josie/development/irc-now/crates/bot/templates/bot_create.html-18-    <label>sasl username</label>
--
/home/josie/development/irc-now/crates/web-landing/static/docs/index.html-48-
/home/josie/development/irc-now/crates/web-landing/static/docs/index.html-49-    <h2>plans</h2>
crates/web-landing/static/docs/index.html:50:    <p>free accounts get 1 network with 48 hours of scrollback on a shared bouncer. pro accounts ($4/mo) get unlimited networks, unlimited scrollback, a dedicated bouncer, and a custom hostname.</p>
/home/josie/development/irc-now/crates/web-landing/static/docs/index.html-51-
/home/josie/development/irc-now/crates/web-landing/static/docs/index.html-52-    <h2>support</h2>
--
/home/josie/development/irc-now/crates/web-landing/static/faq/index.html-41-
/home/josie/development/irc-now/crates/web-landing/static/faq/index.html-42-    <h2>what is the difference between free and pro?</h2>
crates/web-landing/static/faq/index.html:43:    <p>free gets you 1 network, 48 hours of scrollback, and a shared bouncer. pro ($4/mo) gives you unlimited networks, unlimited scrollback, a dedicated bouncer, and a custom hostname for <code>/whois</code>.</p>
/home/josie/development/irc-now/crates/web-landing/static/faq/index.html-44-
/home/josie/development/irc-now/crates/web-landing/static/faq/index.html-45-    <h2>how do I cancel?</h2>
--
/home/josie/development/irc-now/crates/web-api/src/routes/bouncer.rs-146-            .insert(
/home/josie/development/irc-now/crates/web-api/src/routes/bouncer.rs-147-                "irc.now/migrate-from".to_string(),
crates/web-api/src/routes/bouncer.rs:148:                "soju-shared".to_string(),
/home/josie/development/irc-now/crates/web-api/src/routes/bouncer.rs-149-            );
/home/josie/development/irc-now/crates/web-api/src/routes/bouncer.rs-150-    }
--
/home/josie/development/irc-now/crates/web-api/src/routes/bouncer.rs-420-            ).await?;
crates/web-api/src/routes/bouncer.rs-421-            let target = crate::soju_migrate::connect_soju_db(
crates/web-api/src/routes/bouncer.rs:422:                &state.kube, &state.namespace, "soju-shared-db",
/home/josie/development/irc-now/crates/web-api/src/routes/bouncer.rs-423-            ).await?;
crates/web-api/src/routes/bouncer.rs-424-            crate::soju_migrate::reverse_migrate_user(&source, &target, email, &form.keep_network)
--
/home/josie/development/irc-now/crates/web-landing/static/terms/index.html-44-
/home/josie/development/irc-now/crates/web-landing/static/terms/index.html-45-    <h2>7. content expiry</h2>
crates/web-landing/static/terms/index.html:46:    <p>free-tier content (images, pastes) expires automatically. pro content may also expire based on your account settings. we are not responsible for expired content.</p>
/home/josie/development/irc-now/crates/web-landing/static/terms/index.html-47-
/home/josie/development/irc-now/crates/web-landing/static/terms/index.html-48-    <h2>8. termination</h2>