# Network Management Tab (Phase 4 Portal Integration) ## Context The ergo-operator is deployed and reconciles ErgoNetwork CRDs into full IRC server stacks (Deployment, ConfigMap, Service, Route, Certificate, PVC). The platform's own network (net.irc.now) runs as an ErgoNetwork CR. Users currently have no way to create their own IRC networks through the portal -- they need to apply YAML manually. This feature adds a "networks" tab to the account portal so users can create, view, and delete their own Ergo IRC servers. ## Design Decisions - Hostnames: `{name}.irc.now` via wildcard DNS (custom domains deferred) - User config: name + optional display title; everything else platform defaults - Auth: shared `oidc-ergo` Keycloak client (any irc.now user can connect to any network) - Privacy: networks only visible to their owner in the portal (quasi-private by obscurity) - Plan limits: not enforced in v1 ## Implementation Follow the bouncer management pattern exactly. All files in `crates/web-api/`. ### Step 1: Add ErgoNetwork CRD types to `src/k8s.rs` Add below existing SojuBouncer types: - `ErgoNetworkSpec` with fields: hostname, network_name (rename "networkName"), listeners, tls, route, oauth2, cloaking, storage, resources - Supporting types prefixed `Ergo` to avoid collision with bouncer `Listener`: `ErgoListener`, `ErgoTlsSpec`, `ErgoIssuerRef`, `ErgoRouteSpec`, `ErgoOAuth2Spec`, `ErgoCloakingSpec`, `ErgoStorageSpec`, `ErgoResourceRequirements`, `ErgoResourceList` - `ErgoNetworkStatus` with `conditions: Vec` - `ErgoNetworkCondition` with type_, status, reason (Option), message (Option) -- matches `SojuBouncerCondition` pattern - `default_network_spec(name: &str)` -- fills in platform defaults: - hostname: `{name}.irc.now` - network_name: `name` - listeners: `:6697` TLS + `:6667` plain - tls: letsencrypt-prod ClusterIssuer, dns_names: `[{name}.irc.now]` - route: `{name}.irc.now` - oauth2: Keycloak introspection URL, secret_ref `oidc-ergo`, autocreate true - cloaking: netname `{name}.irc.now` - storage: 1Gi, storageClassName `customer-workload-storage` - resources: requests 64Mi/50m, limits 256Mi/200m Reuse existing `sanitize_bouncer_name` for network names (same logic -- lowercase, alphanumeric + dash, trim dashes). Add a `pub fn sanitize_name` alias or just call `sanitize_bouncer_name` directly. Reference: `crates/ergo-operator/src/types.rs` -- web-api types must serialize to identical JSON. Key serde renames: `networkName`, `storageClassName`. Note: `issuer_ref` and `dns_names` are snake_case in both struct and YAML (no rename needed). ### Step 2: Add flash codes to `src/flash.rs` Add to the `resolve()` match block (before `_ => return None`): ``` "network_created" -> Success, "network created." "network_create_failed" -> Error, "failed to create network." "network_name_taken" -> Error, "a network with that name already exists." "network_not_found" -> Error, "network not found." "network_deleted" -> Success, "network deleted." "network_delete_failed" -> Error, "failed to delete network." ``` ### Step 3: Create `src/routes/network.rs` Five handlers mirroring `src/routes/bouncer.rs`: **Templates/types:** - `NetworksTemplate` (networks.html) with `page: PageContext`, `networks: Vec` - `NetworkInfo` with name, hostname, network_name, ready - `NetworkDetailTemplate` (network_detail.html) with page, name, hostname, network_name, ready, status_message - `CreateForm` with name, title (optional) - `NetworkCreatingFragment`, `NetworkStatusFragment`, `FlashFragment` partials **`list()`** -- `Api::namespaced`, filter by `irc.now/owner={sub}` label, render `NetworksTemplate` **`create()`** -- sanitize name, build CR via `ErgoNetwork::new(&name, default_network_spec(&name))`, set `irc.now/owner` label, handle AlreadyExists -> `network_name_taken`, record `network_create` event. If user provided a title, set `spec.network_name = Some(title)`. HTMX returns `NetworkCreatingFragment`, non-JS redirects with flash. **`detail()`** -- get CR by name, verify owner label, extract status, render `NetworkDetailTemplate`. No database query needed (unlike bouncers). **`delete()`** -- verify owner, `api.delete()`, record `network_delete` event, redirect to `/networks`. **`status()`** -- verify owner, return `NetworkStatusFragment`. Emit `hx-trigger: network-ready` when ready. ### Step 4: Register module in `src/routes/mod.rs` Add `pub mod network;` ### Step 5: Register routes in `src/main.rs` Insert after bot routes, before billing routes: ```rust .route("/networks", get(routes::network::list)) .route("/networks/create", post(routes::network::create)) .route("/networks/{name}", get(routes::network::detail)) .route("/networks/{name}/delete", post(routes::network::delete)) .route("/networks/{name}/status", get(routes::network::status)) ``` ### Step 6: Add nav link in `templates/base.html` Add `networks` between "bots" and "billing" links. ### Step 7: Create `templates/networks.html` Modeled on `bouncers.html`. Create form always visible (no plan limits). Each network card links to detail page, shows hostname and ready status. ### Step 8: Create `templates/network_detail.html` Modeled on `bouncer_detail.html`. Cards: status (with HTMX polling), connection info (host, port TLS 6697, port plain 6667, network name, auth method), danger zone (delete with hx-confirm). ### Step 9: Create `templates/partials/network_creating.html` Modeled on `partials/bouncer_creating.html`. Card with name, hostname, status polling. ### Step 10: Create `templates/partials/network_status_fragment.html` Identical pattern to `partials/bouncer_status_fragment.html`. Green "ready" or red "not ready". ## Files Summary | File | Action | |------|--------| | `src/k8s.rs` | Edit: add ErgoNetwork types + default_network_spec | | `src/flash.rs` | Edit: add 6 network flash codes | | `src/routes/network.rs` | Create: 5 handlers | | `src/routes/mod.rs` | Edit: add `pub mod network` | | `src/main.rs` | Edit: register 5 routes | | `templates/base.html` | Edit: add nav link | | `templates/networks.html` | Create | | `templates/network_detail.html` | Create | | `templates/partials/network_creating.html` | Create | | `templates/partials/network_status_fragment.html` | Create | No Cargo.toml changes needed -- all dependencies already present. ## Prerequisites - Wildcard DNS `*.irc.now` configured in Cloudflare (needed for `{name}.irc.now` hostnames to resolve) - Wildcard cert or per-network cert-manager issuance (letsencrypt-prod ClusterIssuer already handles this) ## Verification 1. `cargo build -p web-api` -- compiles 2. `cargo test -p web-api` -- existing + new sanitize tests pass 3. Deploy to cluster, log in to my.irc.now 4. Verify "networks" tab appears in nav 5. Create a network named "test" -- CR appears in cluster, status polls until ready 6. Detail page shows connection info (test.irc.now:6697) 7. Delete network -- CR removed, redirected to /networks with flash 8. Verify platform network (irc-now-net) does not appear in any user's list