# irc.now Project Memory

## Project Overview
IRC bouncer-as-a-service platform. Rust workspace monorepo at ~/development/irc-now.

## Architecture
- **Workspace crates**: common, web-api, web-landing, bot-landing, soju-operator, txt, pics
- **Non-crate services**: chat/ (gamja + nginx, not Rust)
- **Rust**: Axum 0.8, kube.rs 3.0, sqlx, openid 0.23, askama 0.15
- **MSRV**: 1.88 (askama 0.15, kube 3.0, home 0.5.12 require it)
- **Containerfiles**: rust:1.88 -> ubi9-minimal multi-stage builds

## Cluster State (OCP 4.20)
See [cluster.md](cluster.md) for details.

### Namespaces
- `irc-josie-cloud`: web-landing, web-api, bot-landing, accounts-db, soju, soju-operator, paste, pics, minio, paste-db, pics-db, chat
- `irc-keycloak`: keycloak-db (CNPG only, Keycloak deployment moved to `keycloak` namespace)
- `keycloak`: keycloak-operator, irc-now Keycloak CR, josie.cloud Keycloak CR

### Services Live
- `irc.now` -- landing page (web-landing)
- `irc.bot` -- bot landing page (bot-landing)
- `my.irc.now` -- account portal (web-api)
- `txt.irc.now` -- pastebin (paste)
- `irc.pics` -- image host (pics)
- `chat.irc.now` -- web IRC client (gamja + nginx, WebSocket proxy to soju)
- `auth.irc.now` -- Keycloak 26.1 (realm: irc-now, operator-managed CR in `keycloak` ns)

### TLS Pattern
cert-manager Certificate CR -> Secret -> Route `tls.externalCertificate.name`
Requires Role/RoleBinding granting `router` SA in `openshift-ingress` access to the secret.
ClusterIssuer: `letsencrypt-prod` (dns-01 via Cloudflare)

## Build Pattern
OCP binary builds using `--from-archive` with minimal tar (~65K):
```bash
tar czf /tmp/build.tar.gz --exclude='target' --exclude='.git' \
  --exclude='irc-now-landing-page' --exclude='status' --exclude='design' \
  --exclude='./docs' --exclude='notes' Cargo.toml Cargo.lock crates/
oc start-build <name> --from-archive=/tmp/build.tar.gz -n irc-josie-cloud --follow
```
Use `--exclude='./docs'` (not `--exclude='docs'`) to avoid excluding `crates/*/static/docs/`.
BuildConfig needs `dockerfilePath` set (e.g. `crates/web-api/Containerfile`).

## Keycloak Theme
Custom login theme at `deploy/keycloak/theme/irc-now/login/`. Deployed via ConfigMap +
init container on the operator-managed Keycloak CR. Theme extends `keycloak` parent,
overrides template.ftl, login.ftl, login-reset-password.ftl, error.ftl, info.ftl.
To update: edit theme files, recreate ConfigMap, delete the Keycloak pod.
```bash
oc create configmap keycloak-theme-irc-now -n keycloak --from-file=... --dry-run=client -o yaml | oc apply -f -
oc delete pod irc-now-0 -n keycloak
```

## Key Secrets
- `irc-now-keycloak-admin` (keycloak): admin credentials (copied from irc-keycloak)
- `irc-now-keycloak-db` (keycloak): DB credentials (copied from irc-keycloak)
- `keycloak-admin` (irc-keycloak): admin credentials (original)
- `keycloak-db-app` (irc-keycloak): CNPG-managed DB creds
- `accounts-db-app` (irc-josie-cloud): CNPG-managed DB creds
- `oidc-account-portal` (irc-josie-cloud): OIDC client secret
- `oidc-paste` (irc-josie-cloud): OIDC client for paste service
- `oidc-pics` (irc-josie-cloud): OIDC client for pics service
- `stripe-keys` (irc-josie-cloud): secret-key, price-id, webhook-secret (placeholder values -- replace with real keys)
- `minio-credentials` (irc-josie-cloud): root-user, root-password
- `paste-db-app` (irc-josie-cloud): CNPG-managed DB creds
- `pics-db-app` (irc-josie-cloud): CNPG-managed DB creds
- `oidc-soju` (irc-josie-cloud): soju OAuth2 client (client-id=soju, client-secret)

## CNPG Notes
- StorageClass: `customer-workload-storage` (hostPath PVs)
- SELinux context: `container_file_t` required on hostPath dirs
- UID ranges: irc-josie-cloud=1001260000, irc-keycloak=1001300000

## Phase 1 Status
Complete. Plan: docs/plans/2026-03-03-irc-now-phase1-plan.md

## Phase 1.5 Status
Deployed. Auth guard, users table, dashboard/bouncer k8s wiring, Stripe checkout flow,
content pages (docs/clients/faq/what-is-irc), ZNC decommissioned.
Remaining: replace placeholder stripe-keys, configure Upptime (needs GH token perms),
remove znc.josie.lol DNS from Cloudflare.
- `stripe_util.rs` (renamed from `stripe.rs` to avoid shadowing the `stripe` crate)

## Phase 3 Status
Deployed and live. Two new crates: paste (txt.irc.now) and pics (irc.pics).
Auth: OIDC via Keycloak, plan detection via token claims (defaults to free/None).
Paste: nanoid IDs, 24h expiry for free users, 5 paste limit, hourly cleanup task.
Pics: MinIO (S3) storage, thumbnails (300px WebP), 50MB free / 1GB pro storage limit.
Keycloak migrated to operator-managed CR in `keycloak` namespace (was manual Deployment in `irc-keycloak`).
Custom irc-now login theme deployed via ConfigMap + init container.
Logout redirect fixed: derives origin from OIDC_REDIRECT_URL, post-logout URIs configured in Keycloak clients.
- `rust-s3 = "0.35"` for S3 client
- `image = "0.25"` for thumbnail generation
- Axum route ordering: static routes (`/my`) must precede parameterized (`/{id}`)
- SameSite::Lax required on session cookies for cross-site OIDC redirects

## Chat Service (chat.irc.now)
gamja web IRC client served by nginx. Top-level `chat/` directory (not a Rust crate).
- Containerfile clones gamja from `git.sr.ht/~emersion/gamja` (codeberg mirror unreliable)
- nginx serves static files, proxies `/socket` to soju WebSocket (`josie:8080`)
- `proxy_set_header Host $host;` required so soju's WebSocket Origin check passes
- Theme injected via nginx `sub_filter` on `<body>` tag (AGPL compliance)
- Title renamed via `sub_filter 'gamja IRC client' 'chat.irc.now'`
- soju-operator supports `websocket: bool` on Listener type (`ws+insecure://`, `wss://`)
- OAuth2 SSO: gamja -> Keycloak (public client `chat`) -> SASL OAUTHBEARER -> soju
- soju `auth oauth2` with `enable-user-on-auth true`, credentials from `oidc-soju` secret
- Keycloak `chat` client: PKCE disabled (gamja doesn't support it), `exclude.session.state.from.auth.response` and `exclude.issuer.from.auth.response` set to avoid redirect_uri mismatch
- Build: `oc start-build chat --from-dir=chat/ -n irc-josie-cloud --follow`
- Route has `haproxy.router.openshift.io/timeout: 600s` for WebSocket keepalive

## Whois Hostname Cloaking (Deployed)
Design: docs/plans/2026-03-05-whois-hostname-cloaking-design.md
Plan: docs/plans/2026-03-05-whois-hostname-cloaking-plan.md
- CRD has `hostname_cloak: Option<String>`, operator emits `hostname-cloak` in soju.conf
- Custom soju image at `crates/soju-operator/soju/` (Containerfile + apply-hostname-cloak.sh)
- Containerfile clones from Codeberg master, applies hostname-cloak sed patch, builds with nosqlite
- SOJU_IMAGE = internal registry, imagePullPolicy = Always
- All bouncer CRs patched with `hostname_cloak: <name>.irc.now`
- web-api sets hostname_cloak on bouncer creation
- Cloaking applies to **downstream** connections (clients connecting to soju bouncer)
- Does NOT affect upstream network whois (hackint etc. show soju server IP)
- Next: rDNS or network-level cloaks needed for upstream whois

## Soju Version Notes
- Primary repo: Codeberg (codeberg.org/emersion/soju) -- GitHub/SourceHut are stale mirrors
- No v0.10.x tags; Go module proxy serves pseudo-versions for tagged releases only
- Codeberg master has schema v20 (20 PostgreSQL migrations)
- Custom image builds from Codeberg master via `git clone` in Containerfile
- `nosqlite` build tag required for CGO_ENABLED=0 builds (we use PostgreSQL)
- imagePullPolicy must be Always since we use :latest tag

## Obsidian Docs
Service notes at ~/Documents/Obsidian Vault/98 josiedot/services/:
- irc-now-landing.md, irc-now-portal.md, irc-now-keycloak.md, irc-bot-landing.md
- irc-now-chat.md, irc-now-txt.md, irc-now-pics.md, irc-now-soju-operator.md
