diff --git a/INTENT.md b/INTENT.md index 4dc5d58..1477547 100644 --- a/INTENT.md +++ b/INTENT.md @@ -78,7 +78,8 @@ The rebuild is deliberately **product-faithful first**: UI and content parity ma - `docs/adr/ADR-0002-space-content-forgejo-markdown.md` — space content as markdown in Forgejo - `docs/deploy.md` — deploy + spaces operator runbook - `workplans/CSOC-WP-0001-bubble-io-exit-assessment.md` — Bubble inventory/migration **after** product foundation -- `workplans/CSOC-WP-0005-resource-demand-and-cost-evidence.md` — resource evidence for platform cost control +- `workplans/CSOC-WP-0005-resource-demand-and-cost-evidence.md` — **finished**: demand/SLO/labor evidence for resource-control +- `docs/resource-evidence/` — workload demand forecasts, observations, labor - `the-custodian/docs/coulomb-social-rebuild-seed.md` — original workplan seed (CUST-WP-0058-T08) - `the-custodian/canon/standards/business-app-service-contract_v0.1.md` - NetKingdom: IAM Profile, user-engine boundary, KeyCape/Keycloak modes diff --git a/SCOPE.md b/SCOPE.md index 30a5563..5abbe44 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -72,7 +72,8 @@ coulomb.social connects people around shared projects and complementary capabili - Finished identity path: **`CSOC-WP-0002`**, **`CSOC-WP-0003`** (Case A; Case B residual intakes) - Content ADR: `docs/adr/ADR-0002-space-content-forgejo-markdown.md` - Operator: `docs/deploy.md` runbook; residuals `docs/intakes/csoc-residuals.md` -- Open: bulk Bubble migration (`CSOC-WP-0001`); resource evidence (`CSOC-WP-0005`); public registration residuals (`CSOC-IN-0001`/`0002`, NK-WP-0025) +- Finished resource evidence: **`CSOC-WP-0005`** → `docs/resource-evidence/` +- Open: bulk Bubble migration (`CSOC-WP-0001`); public registration residuals (`CSOC-IN-0001`/`0002`, NK-WP-0025) --- diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 26dbf55..e009872 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -12,7 +12,7 @@ | workplan | CSOC-WP-0002 | done | — | workplans/CSOC-WP-0002-netkingdom-user-management-reestablish.md | | workplan | CSOC-WP-0003 | finished | — | workplans/CSOC-WP-0003-self-registration-and-assurance.md | | workplan | CSOC-WP-0004 | finished | — | workplans/CSOC-WP-0004-app-shell-and-space-content.md | -| workplan | CSOC-WP-0005 | ready | — | workplans/CSOC-WP-0005-resource-demand-and-cost-evidence.md | +| workplan | CSOC-WP-0005 | finished | — | workplans/CSOC-WP-0005-resource-demand-and-cost-evidence.md | | task | CSOC-WP-0001-T01 | todo | — | workplans/CSOC-WP-0001-bubble-io-exit-assessment.md | | task | CSOC-WP-0001-T02 | todo | — | workplans/CSOC-WP-0001-bubble-io-exit-assessment.md | | task | CSOC-WP-0001-T03 | wait | — | workplans/CSOC-WP-0001-bubble-io-exit-assessment.md | @@ -36,8 +36,8 @@ | task | CSOC-WP-0004-T05 | done | — | workplans/CSOC-WP-0004-app-shell-and-space-content.md | | task | CSOC-WP-0004-T06 | done | — | workplans/CSOC-WP-0004-app-shell-and-space-content.md | | task | CSOC-WP-0004-T07 | done | — | workplans/CSOC-WP-0004-app-shell-and-space-content.md | -| task | CSOC-WP-0005-T01 | todo | — | workplans/CSOC-WP-0005-resource-demand-and-cost-evidence.md | -| task | CSOC-WP-0005-T02 | todo | — | workplans/CSOC-WP-0005-resource-demand-and-cost-evidence.md | -| task | CSOC-WP-0005-T03 | todo | — | workplans/CSOC-WP-0005-resource-demand-and-cost-evidence.md | +| task | CSOC-WP-0005-T01 | done | — | workplans/CSOC-WP-0005-resource-demand-and-cost-evidence.md | +| task | CSOC-WP-0005-T02 | done | — | workplans/CSOC-WP-0005-resource-demand-and-cost-evidence.md | +| task | CSOC-WP-0005-T03 | done | — | workplans/CSOC-WP-0005-resource-demand-and-cost-evidence.md | | intake | CSOC-IN-0001 | open | blue | docs/intakes/csoc-residuals.md | | intake | CSOC-IN-0002 | open | green | docs/intakes/csoc-residuals.md | diff --git a/docs/resource-evidence/README.md b/docs/resource-evidence/README.md new file mode 100644 index 0000000..769311f --- /dev/null +++ b/docs/resource-evidence/README.md @@ -0,0 +1,30 @@ +# Resource demand and cost evidence + +Workload-owned evidence for +`resource:tenant:coulomb:coulomb-social` (CSOC-WP-0005). + +**Authority boundary** + +| Concern | Owner | +|---------|--------| +| Demand forecasts, app observations, service objectives, app labor | **this repo** | +| Cluster / host capacity and utilization | `railiance-cluster` / `railiance-infra` | +| Shared Postgres (`apps-pg`) | `railiance-platform` | +| Forgejo storage (space markdown SoR) | `railiance-forge` | +| Booked financial facts / invoices | `fin-hub` | +| Portfolio forecast join / optimization | `resource-control` | + +Do **not** put secret values, provider credentials, or invoices here. + +## Documents + +| File | Task | Content | +|------|------|---------| +| `demand-forecast-2026-08.md` | T01 | Narrative + assumptions | +| `demand-forecast-2026-08.json` | T01 | Machine-readable low/base/high proxies | +| `service-objectives-and-observations-2026-08.md` | T02 | SLOs + timestamped observations | +| `service-objectives-and-observations-2026-08.json` | T02 | Stable units for join | +| `workload-operations-labor-2026-08.md` | T03 | Setup + recurring app labor | + +Inventory pointer (resource-control): +`resource-control/data/resources/coulomb-social-production.json`. diff --git a/docs/resource-evidence/demand-forecast-2026-08.json b/docs/resource-evidence/demand-forecast-2026-08.json new file mode 100644 index 0000000..d199ae0 --- /dev/null +++ b/docs/resource-evidence/demand-forecast-2026-08.json @@ -0,0 +1,145 @@ +{ + "schema_version": "0.1", + "record_kind": "workload_demand_forecast", + "resource_id": "resource:tenant:coulomb:coulomb-social", + "service_id": "coulomb-social", + "workload_id": "coulomb-social", + "tenant_id": "tenant:coulomb", + "environment": "production", + "cost_attribution_key": "resource:tenant:coulomb:coulomb-social", + "currency": "EUR", + "forecast_version": "csoc-demand-2026-08-v1", + "created_at": "2026-08-12T09:03:47Z", + "horizon_months": 12, + "planning_period": { + "start": "2026-09-01", + "end": "2027-08-31" + }, + "tenancy_model": { + "deployment_shape": "instance_per_client", + "this_record_scope": "single Coulomb reference instance on shared Railiance capacity", + "current_tenant": "tenant:coulomb", + "external_client_instances": "each future paying client is a separate deployment and its own resource id — not rolled into these scenarios" + }, + "declared_capacity_floor": { + "replicas": 1, + "cpu_request_millicores": 100, + "cpu_limit_millicores": 1000, + "memory_request_mi": 256, + "memory_limit_mi": 1024, + "source": "railiance-apps/charts/coulomb-social/values.yaml" + }, + "proxies": { + "units": { + "active_tenants": "count of tenant_id values with activity in period", + "active_members": "count of Member rows with session activity in period (proxy: monthly active)", + "http_requests_per_month": "application HTTP requests (ingress to app Service)", + "oidc_logins_per_month": "successful OIDC/dev login completions", + "background_jobs_per_month": "async/worker jobs (none in v1 runtime — reserved)", + "app_database_gb": "logical size of coulomb_social_db on apps-pg", + "app_database_growth_pct_per_month": "month-over-month logical DB growth", + "stored_media_gb": "app-owned binary media (PV or object store); Forgejo markdown is NOT counted here", + "forgejo_content_gb_bound": "markdown/assets bound for spaces of this tenant (shared Forgejo service; attribution only)", + "ingress_gb_per_month": "bytes into the app Service", + "egress_gb_per_month": "bytes out of the app pod (HTML + API responses; excludes Forgejo browser edits)", + "cpu_request_millicores": "declared Kubernetes request", + "memory_request_mi": "declared Kubernetes request", + "operator_hours_per_month": "workload-owned ops labor hours (see labor doc)" + } + }, + "scenarios": { + "low": { + "label": "parallel-host operator use (current posture)", + "description": "app.coulomb.social live; apex remains Bubble; handful of operator accounts; no bulk member migration.", + "active_tenants": 1, + "active_members": 5, + "http_requests_per_month": 5000, + "oidc_logins_per_month": 80, + "background_jobs_per_month": 0, + "app_database_gb": 0.1, + "app_database_growth_pct_per_month": 5, + "stored_media_gb": 0, + "forgejo_content_gb_bound": 0.01, + "ingress_gb_per_month": 0.5, + "egress_gb_per_month": 1, + "cpu_request_millicores": 100, + "cpu_limit_millicores": 1000, + "memory_request_mi": 256, + "memory_limit_mi": 1024, + "replicas": 1, + "operator_hours_per_month": 2, + "operator_hourly_eur": 60 + }, + "base": { + "label": "Coulomb community active on rebuild", + "description": "Members and spaces actively used on app host; Bubble still may exist but primary day-to-day for Coulomb operators/members shifts to rebuild; still single tenant:coulomb instance.", + "active_tenants": 1, + "active_members": 50, + "http_requests_per_month": 150000, + "oidc_logins_per_month": 1500, + "background_jobs_per_month": 0, + "app_database_gb": 1, + "app_database_growth_pct_per_month": 10, + "stored_media_gb": 2, + "forgejo_content_gb_bound": 0.5, + "ingress_gb_per_month": 5, + "egress_gb_per_month": 15, + "cpu_request_millicores": 200, + "cpu_limit_millicores": 1000, + "memory_request_mi": 512, + "memory_limit_mi": 1024, + "replicas": 1, + "operator_hours_per_month": 4, + "operator_hourly_eur": 60 + }, + "high": { + "label": "full Bubble parity load on this Coulomb instance", + "description": "All Coulomb spaces/content migrated; peak community concurrency; optional second replica for HA; still NOT multi-client — external clients would be separate instances.", + "active_tenants": 1, + "active_members": 500, + "http_requests_per_month": 2000000, + "oidc_logins_per_month": 20000, + "background_jobs_per_month": 5000, + "app_database_gb": 10, + "app_database_growth_pct_per_month": 15, + "stored_media_gb": 50, + "forgejo_content_gb_bound": 20, + "ingress_gb_per_month": 40, + "egress_gb_per_month": 120, + "cpu_request_millicores": 500, + "cpu_limit_millicores": 2000, + "memory_request_mi": 1024, + "memory_limit_mi": 2048, + "replicas": 2, + "operator_hours_per_month": 8, + "operator_hourly_eur": 60 + } + }, + "monthly_labor_eur_proxy": { + "low": 120.0, + "base": 240.0, + "high": 480.0, + "formula": "operator_hours_per_month × operator_hourly_eur", + "note": "Infrastructure EUR is unknown here (allocated share of cluster/apps-pg/ingress/identity). Leave infrastructure null in cost joins until platform/fin-hub supplies figures." + }, + "assumptions": [ + "Scenarios are per-instance demand for tenant:coulomb, not fleet-wide multi-tenant aggregation.", + "Current production posture (2026-08) matches low: parallel host, identity + shell + spaces MVP, Bubble still on apex.", + "Space page bodies live in Forgejo (ADR-0002); forgejo_content_gb_bound is attribution input for shared Forgejo, not a claim that coulomb-social operates that storage.", + "App media PV is currently disabled in chart; stored_media_gb remains 0 until object storage or PV is enabled.", + "No application worker/queue in v1; background_jobs_per_month is 0 until a job system exists (high reserves headroom).", + "OIDC/identity platform traffic cost is not counted as app infrastructure; only app HTTP path is in http_requests_per_month.", + "Database is coulomb_social_db on shared apps-pg; logical size ownership is workload, physical volume ownership is railiance-platform.", + "CPU/memory figures are declared requirements for rightsizing; observed utilization is cluster authority when sampled.", + "Operator hourly rate €60 matches resource-control backup labor convention for comparability; not a payroll fact.", + "Falsifiable: after each calendar month, compare active_members, http_requests, app_database_gb, and operator_hours against the chosen scenario band." + ], + "source_evidence": [ + "docs/resource-evidence/demand-forecast-2026-08.md", + "docs/adr/ADR-0002-space-content-forgejo-markdown.md", + "docs/deploy.md", + "railiance-apps/charts/coulomb-social/values.yaml", + "resource-control/data/resources/coulomb-social-production.json", + "workplans/CSOC-WP-0005-resource-demand-and-cost-evidence.md" + ] +} diff --git a/docs/resource-evidence/demand-forecast-2026-08.md b/docs/resource-evidence/demand-forecast-2026-08.md new file mode 100644 index 0000000..bd3f5e6 --- /dev/null +++ b/docs/resource-evidence/demand-forecast-2026-08.md @@ -0,0 +1,95 @@ +# Workload demand forecast — coulomb-social (2026-08) + +| Field | Value | +|-------|--------| +| Resource | `resource:tenant:coulomb:coulomb-social` | +| Workplan | CSOC-WP-0005-T01 | +| Forecast version | `csoc-demand-2026-08-v1` | +| Created | 2026-08-12T09:03:47Z | +| Machine record | `demand-forecast-2026-08.json` | + +## Scope + +Demand for **this production instance** (Coulomb reference tenant on +Railiance). Deployment shape is **instance-per-client** (DR-1 C): future +external customers are **separate instances** and separate resource ids — +they are **not** folded into the high scenario. + +| Case | What it models | +|------|----------------| +| **low** | Current parallel-host posture: few operators, Bubble still full product on apex | +| **base** | Coulomb community day-to-day on the rebuild (still one tenant) | +| **high** | Full Bubble-parity load on this Coulomb instance (HA-ish replicas/resources) | + +## Proxies (stable units) + +| Proxy | Unit | Notes | +|-------|------|--------| +| active_tenants | count | Expected 1 for this instance | +| active_members | MAU-ish Member count | App-owned | +| http_requests_per_month | count | App Service ingress | +| oidc_logins_per_month | count | Successful app logins | +| background_jobs_per_month | count | 0 until workers exist | +| app_database_gb | GB logical | `coulomb_social_db` on apps-pg | +| stored_media_gb | GB | App-owned binaries only | +| forgejo_content_gb_bound | GB | Attribution for space markdown on shared Forgejo | +| ingress_gb / egress_gb | GB/month | App path only | +| cpu/memory request·limit | mCPU / Mi | Declared K8s requirements | +| operator_hours_per_month | hours | Workload labor (T03) | + +## Scenario table (monthly) + +| Proxy | low | base | high | +|-------|----:|-----:|-----:| +| active_tenants | 1 | 1 | 1 | +| active_members | 5 | 50 | 500 | +| http_requests | 5 000 | 150 000 | 2 000 000 | +| oidc_logins | 80 | 1 500 | 20 000 | +| background_jobs | 0 | 0 | 5 000 | +| app_database_gb | 0.1 | 1 | 10 | +| DB growth %/mo | 5 | 10 | 15 | +| stored_media_gb | 0 | 2 | 50 | +| forgejo_content_gb_bound | 0.01 | 0.5 | 20 | +| ingress_gb | 0.5 | 5 | 40 | +| egress_gb | 1 | 15 | 120 | +| cpu request (m) | 100 | 200 | 500 | +| mem request (Mi) | 256 | 512 | 1024 | +| replicas | 1 | 1 | 2 | +| operator h/mo | 2 | 4 | 8 | +| labor €/mo @ €60/h | 120 | 240 | 480 | + +Infrastructure EUR is **not invented** here. Cost joins should treat +infrastructure as an allocated share of cluster, apps-pg, ingress, and +identity platform once those owners and fin-hub supply figures. + +## Capacity floor (current deploy declaration) + +From `railiance-apps` chart defaults (production values do not override +resources as of 2026-08): + +| Dimension | Value | +|-----------|--------| +| replicas | 1 | +| cpu request / limit | 100m / 1000m | +| memory request / limit | 256Mi / 1Gi | +| media PV | disabled | +| Image tag (values) | `7fcd0cf` (railiance-apps helm values; may lag git HEAD) | + +## How resource-control should use this + +1. Pick scenario band from observed MAU / requests (or default **low** until + Bubble migration starts). +2. Feed proxies into monthly forecast/actual control (null infrastructure + until platform prices exist). +3. Attribute Forgejo-bound content via `forgejo_content_gb_bound` without + double-counting storage in the app resource. +4. Recalibrate after three comparable months (MAPE on members, requests, DB + GB, labor hours). + +## Assumptions and falsifiability + +See JSON `assumptions` array. Primary falsifiers after each calendar month: + +- measured `active_members` outside the active scenario band by >2× +- `app_database_gb` growth vs declared growth % +- operator hours vs T03 labor log diff --git a/docs/resource-evidence/service-objectives-and-observations-2026-08.json b/docs/resource-evidence/service-objectives-and-observations-2026-08.json new file mode 100644 index 0000000..a3367d1 --- /dev/null +++ b/docs/resource-evidence/service-objectives-and-observations-2026-08.json @@ -0,0 +1,210 @@ +{ + "schema_version": "0.1", + "record_kind": "service_objectives_and_usage_observations", + "resource_id": "resource:tenant:coulomb:coulomb-social", + "service_id": "coulomb-social", + "workload_id": "coulomb-social", + "tenant_id": "tenant:coulomb", + "environment": "production", + "cost_attribution_key": "resource:tenant:coulomb:coulomb-social", + "created_at": "2026-08-12T09:03:47Z", + "service_objectives": { + "availability": { + "target": "99.5_percent_monthly", + "scope": "HTTPS app.coulomb.social application responses excluding scheduled maintenance and dependency outages (OIDC, apps-pg, cluster)", + "measurement": "synthetic GET /healthz success ratio + ingress 5xx from platform telemetry when available", + "notes": "Not a contractual SLA; planning objective for infrastructure options." + }, + "latency": { + "healthz_p99_ms": 500, + "authenticated_html_p95_ms": 1500, + "space_markdown_render_p95_ms": 3000, + "notes": "Markdown path includes Forgejo fetch; cold cache may exceed target — track separately." + }, + "recovery": { + "rpo_minutes": 60, + "rto_minutes": 240, + "notes": "RPO/RTO for app DB via apps-pg backup ownership; content RPO/RTO inherits Forgejo backup policy for bound repos." + }, + "retention": { + "app_database_backup_days": 30, + "application_logs_days": 14, + "session_cookie": "browser session; no long-lived app refresh tokens in v1", + "notes": "Backup retention executed by apps-pg/platform; app requires ≥30 days logical recoverability." + }, + "tenant_isolation": { + "model": "tenant_id column isolation within instance + instance_per_client deployments", + "default_tenant": "tenant:coulomb", + "cross_tenant_list": "forbidden — Space queries filter by session tenant_id", + "identity": "NetKingdom OIDC (issuer, subject) Member binding; no local passwords" + }, + "security": { + "authn": "OIDC public client + session cookies Secure/HttpOnly/SameSite=Lax in prod", + "secrets": "K8s Secret coulomb-social-env / OpenBao; never in git", + "content_xss": "bleach-sanitized markdown HTML" + } + }, + "observations": [ + { + "name": "public_healthz", + "value": "ok", + "unit": "status", + "observed_at": "2026-08-12T09:03:47Z", + "method": "curl -fsS https://app.coulomb.social/healthz", + "authority": "coulomb-social" + }, + { + "name": "public_landing_http_status", + "value": "200", + "unit": "http_status", + "observed_at": "2026-08-12T09:03:47Z", + "method": "curl -o /dev/null -w status https://app.coulomb.social/", + "authority": "coulomb-social" + }, + { + "name": "demo_fixture_raw_bytes", + "value": "586", + "unit": "bytes", + "observed_at": "2026-08-12T09:03:47Z", + "method": "GET Forgejo raw docs/space-fixtures/demo/pages/index.md", + "authority": "coulomb-social + forgejo.coulomb.social public raw" + }, + { + "name": "declared_replicas", + "value": "1", + "unit": "count", + "observed_at": "2026-08-12", + "method": "chart values replicaCount", + "authority": "railiance-apps/charts/coulomb-social/values.yaml" + }, + { + "name": "declared_cpu_request_millicores", + "value": "100", + "unit": "millicores", + "observed_at": "2026-08-12", + "method": "chart resources.requests.cpu", + "authority": "railiance-apps" + }, + { + "name": "declared_memory_request_mi", + "value": "256", + "unit": "Mi", + "observed_at": "2026-08-12", + "method": "chart resources.requests.memory", + "authority": "railiance-apps" + }, + { + "name": "declared_cpu_limit_millicores", + "value": "1000", + "unit": "millicores", + "observed_at": "2026-08-12", + "method": "chart resources.limits.cpu", + "authority": "railiance-apps" + }, + { + "name": "declared_memory_limit_mi", + "value": "1024", + "unit": "Mi", + "observed_at": "2026-08-12", + "method": "chart resources.limits.memory", + "authority": "railiance-apps" + }, + { + "name": "media_persistence_enabled", + "value": "false", + "unit": "bool", + "observed_at": "2026-08-12", + "method": "chart persistence.media.enabled", + "authority": "railiance-apps" + }, + { + "name": "helm_image_tag", + "value": "7fcd0cf", + "unit": "git_sha_short", + "observed_at": "2026-08-12", + "method": "helm/coulomb-social-values.yaml image.tag", + "authority": "railiance-apps" + }, + { + "name": "portfolio_ready_replicas", + "value": "1", + "unit": "count", + "observed_at": "2026-08-11", + "method": "Kubernetes API sample in RESOURCE-WP-0003 initial portfolio discovery", + "authority": "resource-control discovery (reef-railiance)" + }, + { + "name": "commissioned_on", + "value": "2026-08-09", + "unit": "date", + "observed_at": "2026-08-11", + "method": "inventory record lifecycle.commissioned_on", + "authority": "resource-control/data/resources/coulomb-social-production.json" + }, + { + "name": "app_database_gb_logical", + "value": "unknown", + "unit": "GB", + "observed_at": "2026-08-12", + "method": "not measured this session (kubectl/apps-pg access unavailable)", + "authority": "coulomb-social — gap; platform may supply" + }, + { + "name": "production_member_count", + "value": "unknown", + "unit": "count", + "observed_at": "2026-08-12", + "method": "not queried (no production DB shell this session)", + "authority": "coulomb-social — gap" + }, + { + "name": "observed_cpu_millicores", + "value": "unknown", + "unit": "millicores", + "observed_at": "2026-08-12", + "method": "metrics-server sample unavailable this session", + "authority": "railiance-cluster when sampled" + }, + { + "name": "observed_memory_mi", + "value": "unknown", + "unit": "Mi", + "observed_at": "2026-08-12", + "method": "metrics-server sample unavailable this session", + "authority": "railiance-cluster when sampled" + }, + { + "name": "local_dev_member_count", + "value": "2", + "unit": "count", + "observed_at": "2026-08-12T09:03:47Z", + "method": "local sqlite Member.objects.count() after pytest/dev use — not production", + "authority": "coulomb-social workstation (non-prod)" + } + ], + "dependency_slo_inheritance": [ + { + "dependency": "resource:railiance:apps-pg", + "inherits": ["database backup", "DB RPO/RTO physical path"] + }, + { + "dependency": "resource:railiance:reef-railiance:k3s", + "inherits": ["node availability", "pod scheduling"] + }, + { + "dependency": "resource:railiance:forgejo", + "inherits": ["space markdown durability", "git history"] + }, + { + "dependency": "NetKingdom KeyCape / Authelia", + "inherits": ["authentication availability", "MFA path"] + } + ], + "source_evidence": [ + "docs/resource-evidence/service-objectives-and-observations-2026-08.md", + "docs/deploy.md", + "docs/identity/smoke.md", + "docs/adr/ADR-0001-netkingdom-identity.md", + "docs/adr/ADR-0002-space-content-forgejo-markdown.md" + ] +} diff --git a/docs/resource-evidence/service-objectives-and-observations-2026-08.md b/docs/resource-evidence/service-objectives-and-observations-2026-08.md new file mode 100644 index 0000000..93229ca --- /dev/null +++ b/docs/resource-evidence/service-objectives-and-observations-2026-08.md @@ -0,0 +1,72 @@ +# Service objectives and usage observations — coulomb-social (2026-08) + +| Field | Value | +|-------|--------| +| Resource | `resource:tenant:coulomb:coulomb-social` | +| Workplan | CSOC-WP-0005-T02 | +| Created | 2026-08-12T09:03:47Z | +| Machine record | `service-objectives-and-observations-2026-08.json` | + +## Service objectives (application-owned) + +These are **planning requirements** for evaluating infrastructure options and +forecast error — not external contractual SLAs. + +| Domain | Objective | Notes | +|--------|-----------|--------| +| Availability | 99.5% monthly for app HTTPS | Excludes scheduled maintenance and pure dependency outages | +| Latency | `/healthz` p99 ≤ 500 ms | Synthetic-friendly | +| Latency | Auth HTML p95 ≤ 1.5 s | App home, session pages | +| Latency | Space markdown p95 ≤ 3 s | Includes Forgejo fetch; cold cache separate | +| Recovery | RPO ≤ 60 min, RTO ≤ 4 h | DB via apps-pg; content via Forgejo policy | +| Retention | DB backups ≥ 30 days | Executed by platform; app requires recoverability | +| Retention | App logs ≥ 14 days | Platform log sink when wired | +| Isolation | `tenant_id` filter + instance-per-client | No cross-tenant list/detail | +| Auth | NetKingdom OIDC; no local passwords | ADR-0001 | +| Content safety | Sanitized markdown HTML | ADR-0002 | + +### Dependency inheritance (not double-owned) + +| Dependency | What we inherit | +|------------|-----------------| +| apps-pg | Physical DB backup/restore mechanics | +| reef-railiance k3s | Node/pod availability | +| Forgejo | Markdown durability and git history | +| KeyCape / Authelia | Sign-in path availability | + +## Observations (timestamped, non-secret) + +| Measure | Value | When | Authority | +|---------|-------|------|-----------| +| public_healthz | ok | 2026-08-12T09:03:47Z | curl app.coulomb.social | +| public_landing_http_status | 200 | 2026-08-12T09:03:47Z | curl | +| demo_fixture_raw_bytes | 586 | 2026-08-12T09:03:47Z | Forgejo public raw | +| declared_replicas | 1 | 2026-08-12 | railiance-apps chart | +| declared_cpu_request_millicores | 100 | 2026-08-12 | chart | +| declared_memory_request_mi | 256 | 2026-08-12 | chart | +| declared_cpu_limit_millicores | 1000 | 2026-08-12 | chart | +| declared_memory_limit_mi | 1024 | 2026-08-12 | chart | +| media_persistence_enabled | false | 2026-08-12 | chart | +| helm_image_tag | 7fcd0cf | 2026-08-12 | helm values | +| portfolio_ready_replicas | 1 | 2026-08-11 | RESOURCE-WP-0003 discovery | +| commissioned_on | 2026-08-09 | inventory | resource-control | +| app_database_gb_logical | **unknown** | 2026-08-12 | needs apps-pg measure | +| production_member_count | **unknown** | 2026-08-12 | needs prod DB shell | +| observed_cpu/memory | **unknown** | 2026-08-12 | cluster metrics authority | + +Explicit **unknown** is preferred over invented utilization. Cluster samples +belong in railiance-cluster evidence; logical DB size may be published here +once an operator can run a non-secret `pg_database_size` (or app management +command) without credentials in git. + +## Forecast error measurement + +After each month, compare demand proxies (T01) to: + +1. request/login counts from ingress or app metrics (when exported), +2. logical DB size, +3. member/space counts from app DB, +4. operator hours from T03. + +Record actuals as a dated observation append (new file or JSON revision), never +rewrite prior observations. diff --git a/docs/resource-evidence/workload-operations-labor-2026-08.md b/docs/resource-evidence/workload-operations-labor-2026-08.md new file mode 100644 index 0000000..32c1c25 --- /dev/null +++ b/docs/resource-evidence/workload-operations-labor-2026-08.md @@ -0,0 +1,81 @@ +# Workload operations labor — coulomb-social (2026-08) + +| Field | Value | +|-------|--------| +| Resource | `resource:tenant:coulomb:coulomb-social` | +| Workplan | CSOC-WP-0005-T03 | +| Created | 2026-08-12T09:03:47Z | +| Labor rate (planning) | €60 / hour (resource-control convention; not payroll) | + +## Boundary — count only app workload labor here + +| In scope (coulomb-social) | Out of scope (do not double-count) | +|---------------------------|-------------------------------------| +| App image build/publish for this service | k3s/node host ops (`railiance-infra` / cluster) | +| Helm values / release for coulomb-social | Shared apps-pg admin & backups (`railiance-platform`) | +| Django migrations for this app | Ingress controller / cert-manager fleet work | +| Seed/bind spaces, Forgejo webhook config for app | Forgejo platform upgrades (`railiance-forge`) | +| App incident triage (500s, bad deploy, data bugs) | KeyCape / Authelia / LLDAP platform incidents | +| App restore drill participation (app checklist) | Cluster restore of node/volumes | +| Content/model product ops (spaces ADR, smoke) | Bubble.io hosting (external product until cutover) | + +## Setup labor (one-time / infrequent) + +Estimates for standing up or re-standing the **Coulomb** production instance. +Recorded for total-cost models; not a timesheet. + +| Activity | Hours | Cadence | Notes | +|----------|------:|---------|--------| +| Scaffold app + identity shell (historical CSOC-WP-0002) | 40 | one-time done | sunk; for amortization models only | +| App shell + spaces + Forgejo path (CSOC-WP-0004) | 24 | one-time done | sunk | +| KeyCape client registration / redirect updates | 1 | per host change | script: `scripts/register-keycape-client.sh` | +| Env secret create/rotate (names via railiance-apps tooling) | 0.5 | per rotate | operator; no secret values in repo | +| First deploy + ingress + DNS for app.coulomb.social | 2 | one-time done | with railiance-apps | +| Demo space seed + smoke | 0.5 | per env | `seed_demo_space` + browser checklist | +| Public registration enablement when NK ready | 2 | once | residual CSOC-IN-0001 | +| Bubble migration rehearsal (future CSOC-WP-0001) | 16 | per rehearsal | not yet executed | + +**Setup already sunk (approx):** ~66.5 h for current parallel-host MVP +**Near-term setup residual:** ~2.5 h (seed/smoke + registration enable) +**Migration rehearsal (planned):** ~16 h when inventory/mapping ready + +## Recurring labor (monthly, steady state) + +Aligned with demand scenarios in `demand-forecast-2026-08.json`. + +| Activity | low h | base h | high h | Notes | +|----------|------:|-------:|-------:|--------| +| Release: build image, bump tag, deploy, smoke | 1.0 | 1.5 | 2.5 | commit-SHA images | +| Migration apply + verify | 0.25 | 0.5 | 1.0 | only when schema changes | +| Space content ops (seed/bind/webhook, content incidents) | 0.25 | 0.75 | 2.0 | Forgejo is SoR; app refresh path | +| Identity smoke / session diagnostics | 0.25 | 0.5 | 1.0 | Case A; Case B when live | +| Incident response (app-owned) | 0.25 | 0.5 | 1.5 | excludes pure platform outages | +| Recovery exercise (app checklist against restored DB) | 0 | 0.25 | 0.5 | quarterly average as monthly | +| Demand/evidence refresh | 0 | 0 | 0.5 | forecast vs actual notes | +| **Total recurring** | **2.0** | **4.0** | **8.0** | matches T01 operator_hours | + +### Monthly labor € proxy (internal only) + +| Scenario | Hours | € @ 60 | +|----------|------:|-------:| +| low | 2 | 120 | +| base | 4 | 240 | +| high | 8 | 480 | + +## What not to bill twice + +- **Cluster upgrade weekend** → railiance-cluster / infra labor, even if app is + redeployed as a consumer. +- **apps-pg vacuum/backup failure** → platform labor; app only spends the hours + verifying app health after restore. +- **Forgejo disk full** → forge labor; app may open an incident for missing + content but content storage is not app labor ownership. +- **Authelia MFA outage** → NetKingdom; app documents customer impact only. + +## Evidence links + +- Operator runbook: `docs/deploy.md` +- Identity smoke: `docs/identity/smoke.md` +- Spaces content: `docs/spaces-content.md` +- Residuals: `docs/intakes/csoc-residuals.md` +- Demand: `docs/resource-evidence/demand-forecast-2026-08.md` diff --git a/workplans/CSOC-WP-0005-resource-demand-and-cost-evidence.md b/workplans/CSOC-WP-0005-resource-demand-and-cost-evidence.md index 8b56a6c..4c16404 100644 --- a/workplans/CSOC-WP-0005-resource-demand-and-cost-evidence.md +++ b/workplans/CSOC-WP-0005-resource-demand-and-cost-evidence.md @@ -4,11 +4,11 @@ type: workplan title: "Publish Coulomb Social resource demand and cost evidence" domain: communication repo: coulomb-social -status: ready +status: finished owner: codex topic_slug: coulomb-social created: "2026-08-11" -updated: "2026-08-11" +updated: "2026-08-12" related: - CSOC-WP-0004 - RESOURCE-WP-0003 @@ -24,11 +24,13 @@ Publish workload-owned requirements and observations for or financial authority into this repository. Origin: `RESOURCE-WP-0003-T04` delegated evidence gap. +**Finished 2026-08-12:** evidence under `docs/resource-evidence/`. + ## T01 — Declare low, base, and high workload demand ```task id: CSOC-WP-0005-T01 -status: todo +status: done priority: high state_hub_task_id: "d22e42e7-186e-4206-9cf4-c533d6388b13" ``` @@ -40,11 +42,15 @@ multi-tenant scenarios. Done when resource-control can produce falsifiable monthly demand forecasts. +2026-08-12: `docs/resource-evidence/demand-forecast-2026-08.{md,json}` — +low/base/high with units, assumptions, instance-per-client scope (external +clients = separate instances), labor € proxy, capacity floor from chart. + ## T02 — Publish service objectives and usage observations ```task id: CSOC-WP-0005-T02 -status: todo +status: done priority: high state_hub_task_id: "ac078211-3cd9-49d8-baac-c0a1b60278fb" ``` @@ -57,11 +63,15 @@ owners. Done when infrastructure options can be evaluated against application-owned requirements and forecast error can be measured. +2026-08-12: `docs/resource-evidence/service-objectives-and-observations-2026-08.{md,json}` +— SLOs + timestamped measures; explicit **unknown** for prod DB size, member +count, and live CPU/memory (cluster/DB authority). + ## T03 — Record workload operations labor ```task id: CSOC-WP-0005-T03 -status: todo +status: done priority: medium state_hub_task_id: "a15e630c-6bec-4c50-9622-bce16e1ce418" ``` @@ -73,8 +83,16 @@ cluster, database, identity, and provider labor. Done when resource-control can include workload labor without double counting platform operations. +2026-08-12: `docs/resource-evidence/workload-operations-labor-2026-08.md` — +setup sunk vs residual, recurring low/base/high hours matching demand, out-of +scope table for platform labor. + ## Acceptance -- [ ] Demand forecasts distinguish current, growth, and external-tenant cases. -- [ ] Service objectives and observations have stable units and provenance. -- [ ] Workload labor is separated from delegated infrastructure labor. +- [x] Demand forecasts distinguish current, growth, and external-tenant cases. +- [x] Service objectives and observations have stable units and provenance. +- [x] Workload labor is separated from delegated infrastructure labor. + +## Index + +`docs/resource-evidence/README.md`