+ {{- end }}
+ {{- if .Values.landing.links }}
+
+ {{- end }}
+
+
+
+{{ end }}
+{{- end }}
diff --git a/charts/reuse-surface/templates/landing-deployment.yaml b/charts/reuse-surface/templates/landing-deployment.yaml
new file mode 100644
index 0000000..a8bdb6f
--- /dev/null
+++ b/charts/reuse-surface/templates/landing-deployment.yaml
@@ -0,0 +1,59 @@
+{{- if .Values.landing.enabled }}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ include "reuse.landingFullname" . }}
+ labels:
+ {{- include "reuse.labels" . | nindent 4 }}
+ app.kubernetes.io/component: landing
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ {{- include "reuse.landingSelectorLabels" . | nindent 6 }}
+ strategy:
+ type: RollingUpdate
+ rollingUpdate:
+ maxSurge: 1
+ maxUnavailable: 0
+ template:
+ metadata:
+ labels:
+ {{- include "reuse.landingSelectorLabels" . | nindent 8 }}
+ spec:
+ enableServiceLinks: false
+ containers:
+ - name: landing
+ image: {{ include "reuse.landingImage" . | quote }}
+ imagePullPolicy: {{ .Values.landing.image.pullPolicy }}
+ ports:
+ - name: http
+ containerPort: {{ .Values.landing.service.targetPort }}
+ protocol: TCP
+ readinessProbe:
+ httpGet:
+ path: /
+ port: http
+ initialDelaySeconds: 3
+ periodSeconds: 10
+ timeoutSeconds: 3
+ failureThreshold: 3
+ livenessProbe:
+ httpGet:
+ path: /
+ port: http
+ initialDelaySeconds: 10
+ periodSeconds: 30
+ timeoutSeconds: 3
+ failureThreshold: 3
+ resources: {{- toYaml .Values.landing.resources | nindent 12 }}
+ volumeMounts:
+ - name: landing-page
+ mountPath: /usr/share/nginx/html/index.html
+ subPath: index.html
+ readOnly: true
+ volumes:
+ - name: landing-page
+ configMap:
+ name: {{ include "reuse.landingFullname" . }}
+{{- end }}
diff --git a/charts/reuse-surface/templates/landing-service.yaml b/charts/reuse-surface/templates/landing-service.yaml
new file mode 100644
index 0000000..927e331
--- /dev/null
+++ b/charts/reuse-surface/templates/landing-service.yaml
@@ -0,0 +1,18 @@
+{{- if .Values.landing.enabled }}
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ include "reuse.landingFullname" . }}
+ labels:
+ {{- include "reuse.labels" . | nindent 4 }}
+ app.kubernetes.io/component: landing
+spec:
+ type: ClusterIP
+ selector:
+ {{- include "reuse.landingSelectorLabels" . | nindent 4 }}
+ ports:
+ - name: http
+ port: {{ .Values.landing.service.port }}
+ targetPort: http
+ protocol: TCP
+{{- end }}
diff --git a/charts/reuse-surface/templates/redirect-ingress.yaml b/charts/reuse-surface/templates/redirect-ingress.yaml
new file mode 100644
index 0000000..9aa05ed
--- /dev/null
+++ b/charts/reuse-surface/templates/redirect-ingress.yaml
@@ -0,0 +1,23 @@
+{{- if and .Values.ingress.enabled .Values.ingress.redirectHttp.enabled }}
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: {{ include "reuse.fullname" . }}-http-redirect
+ labels: {{- include "reuse.labels" . | nindent 4 }}
+ annotations:
+ traefik.ingress.kubernetes.io/router.entrypoints: web
+ traefik.ingress.kubernetes.io/router.middlewares: {{ printf "%s-%s@kubernetescrd" .Release.Namespace (include "reuse.redirectMiddlewareName" .) | quote }}
+spec:
+ ingressClassName: {{ .Values.ingress.className }}
+ rules:
+ - host: {{ .Values.ingress.host }}
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: {{ include "reuse.fullname" . }}
+ port:
+ number: {{ .Values.service.port }}
+{{- end }}
diff --git a/charts/reuse-surface/templates/redirect-middleware.yaml b/charts/reuse-surface/templates/redirect-middleware.yaml
new file mode 100644
index 0000000..42bd2a7
--- /dev/null
+++ b/charts/reuse-surface/templates/redirect-middleware.yaml
@@ -0,0 +1,11 @@
+{{- if and .Values.ingress.enabled .Values.ingress.redirectHttp.enabled }}
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+ name: {{ include "reuse.redirectMiddlewareName" . }}
+ labels: {{- include "reuse.labels" . | nindent 4 }}
+spec:
+ redirectScheme:
+ scheme: https
+ permanent: {{ .Values.ingress.redirectHttp.permanent }}
+{{- end }}
diff --git a/charts/reuse-surface/templates/service.yaml b/charts/reuse-surface/templates/service.yaml
index 6ad53b9..906e3fb 100644
--- a/charts/reuse-surface/templates/service.yaml
+++ b/charts/reuse-surface/templates/service.yaml
@@ -5,9 +5,11 @@ metadata:
labels: {{- include "reuse.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
- selector: {{- include "reuse.selectorLabels" . | nindent 4 }}
+ selector:
+ {{- include "reuse.selectorLabels" . | nindent 4 }}
+ app.kubernetes.io/component: api
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: http
- protocol: TCP
\ No newline at end of file
+ protocol: TCP
diff --git a/charts/reuse-surface/values.yaml b/charts/reuse-surface/values.yaml
index de526c7..55ceb1d 100644
--- a/charts/reuse-surface/values.yaml
+++ b/charts/reuse-surface/values.yaml
@@ -26,11 +26,48 @@ resources:
envSecretName: reuse-surface-env
+landing:
+ enabled: false
+ image:
+ repository: nginxinc/nginx-unprivileged
+ tag: "1.27-alpine"
+ pullPolicy: IfNotPresent
+ service:
+ port: 8080
+ targetPort: 8080
+ noindex: true
+ title: "Railiance service endpoint"
+ eyebrow: "Railiance S5"
+ body: "This endpoint is available for automated clients and operators."
+ buttonLabel: "Continue"
+ primaryUrl: ""
+ redirect:
+ enabled: false
+ target: ""
+ delaySeconds: 5
+ links: []
+ html: ""
+ resources:
+ requests:
+ cpu: 10m
+ memory: 32Mi
+ limits:
+ cpu: 50m
+ memory: 64Mi
+
ingress:
enabled: true
className: traefik
host: reuse.coulomb.social
tls: true
+ redirectHttp:
+ enabled: false
+ permanent: true
+ apiPaths:
+ - path: /health
+ pathType: Exact
+ - path: /v1
+ pathType: Prefix
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
@@ -56,4 +93,4 @@ securityContext: {}
nodeSelector: {}
tolerations: []
-affinity: {}
\ No newline at end of file
+affinity: {}
diff --git a/docs/reuse-surface-on-railiance01.md b/docs/reuse-surface-on-railiance01.md
index 1516575..a0b6d6c 100644
--- a/docs/reuse-surface-on-railiance01.md
+++ b/docs/reuse-surface-on-railiance01.md
@@ -42,8 +42,30 @@ export REUSE_SURFACE_URL=http://127.0.0.1:18001
| Chart | `charts/reuse-surface` |
| Values | `helm/reuse-surface-values.yaml` |
| Image | `gitea.coulomb.social/coulomb/reuse-surface:` |
+| Landing image | `nginxinc/nginx-unprivileged:1.27-alpine` |
| Secret | `reuse-surface-env` (`REUSE_SURFACE_TOKEN`) |
+## Browser landing page
+
+`https://reuse.coulomb.social/` serves a static no-login landing page from the
+Helm-managed `reuse-surface-landing` Deployment and Service. It exists for
+humans who open the hostname in a browser; it does not change the API service.
+
+Ingress routing is intentionally split:
+
+- HTTP `/` redirects permanently to `https://reuse.coulomb.social/`;
+- HTTPS `/health` and `/v1/*` route to `svc/reuse-surface`;
+- HTTPS `/` and other non-API browser paths route to
+ `svc/reuse-surface-landing`.
+
+The rendered page includes `noindex,nofollow`, a short service description, and
+links to `/health`, `/v1/federated`, and this operator runbook. It must not
+include `REUSE_SURFACE_TOKEN` or any other runtime secret.
+
+Rollback: set `landing.enabled: false` in `helm/reuse-surface-values.yaml` and
+run `KUBECONFIG=~/.kube/config-hosteurope make reuse-deploy`; the ingress will
+return to routing all `/` traffic to the API service.
+
## Deploy
```bash
@@ -67,7 +89,10 @@ KUBECONFIG=~/.kube/config-hosteurope make reuse-status
## Smoke checks
```bash
+curl -I http://reuse.coulomb.social/
+curl -k --resolve reuse.coulomb.social:443:92.205.62.239 https://reuse.coulomb.social/
curl -k --resolve reuse.coulomb.social:443:92.205.62.239 https://reuse.coulomb.social/health
+curl -k --resolve reuse.coulomb.social:443:92.205.62.239 https://reuse.coulomb.social/v1/federated
export REUSE_SURFACE_TOKEN=$(KUBECONFIG=~/.kube/config-hosteurope kubectl get secret reuse-surface-env -n reuse \
-o jsonpath='{.data.REUSE_SURFACE_TOKEN}' | base64 -d)
@@ -95,4 +120,4 @@ KUBECONFIG=~/.kube/config-hosteurope make reuse-deploy
```
Bootstrap copy on CoulombCore (`92.205.130.254`) was removed 2026-06-15 — use
-`config-hosteurope` only.
\ No newline at end of file
+`config-hosteurope` only.
diff --git a/docs/s5-app-onboarding-checklist.md b/docs/s5-app-onboarding-checklist.md
index 642bb96..eb054e5 100644
--- a/docs/s5-app-onboarding-checklist.md
+++ b/docs/s5-app-onboarding-checklist.md
@@ -64,6 +64,40 @@ workplans first.
headers to a value included in the app's allowed hosts.
- [ ] Keep readiness and liveness paths stable and unauthenticated.
+## Endpoint Landing Pages
+
+- [ ] Give every public S5 endpoint an intentional browser response at `/`.
+- [ ] For API-only services, serve a static informational landing page at `/`
+ that states the service purpose, avoids any login claim, does not expose or
+ hint at runtime secrets, and links only to non-secret health/status or
+ operator documentation.
+- [ ] For UI-backed services, a landing page may forward users to the canonical
+ login or application route, but it must also include a visible button for the
+ same destination.
+- [ ] Preserve existing machine-facing paths. Health probes, API prefixes,
+ OAuth callbacks, and static asset routes must continue to reach their owning
+ backend after the landing page is enabled.
+- [ ] For API-only endpoints, route explicit API/probe paths such as `/health`
+ and `/v1` to the API service, then use `/` as the landing fallback. Avoid
+ competing exact `/` and prefix `/` rules for different backends.
+- [ ] Add `noindex` metadata for operator and service landing pages that are
+ not intended as public marketing pages.
+
+Example UI-backed landing values:
+
+```yaml
+landing:
+ enabled: true
+ title: "Application sign-in"
+ eyebrow: "app.example.coulomb.social"
+ body: "You are being sent to the application sign-in page."
+ buttonLabel: "Continue to sign-in"
+ redirect:
+ enabled: true
+ target: "/login/"
+ delaySeconds: 5
+```
+
## Validation And Smoke Tests
- [ ] Run `make check-tools`.
@@ -73,8 +107,8 @@ workplans first.
- [ ] Use the persistent-pod plus `kubectl exec` smoke pattern from
`docs/operator-recipes.md`.
- [ ] Capture app-level deployment evidence: dry-run result, rollout status,
- HTTPS or service smoke check, migration result when applicable, and rollback
- note.
+ HTTPS or service smoke check, landing-page check when enabled, migration
+ result when applicable, and rollback note.
## Runbook Baseline
@@ -86,6 +120,7 @@ Each S5 app runbook should include:
- day-to-day operator commands;
- image promotion steps;
- rollback behavior and migration warning;
+- public `/` landing-page behavior and the canonical login or API entrypoints;
- troubleshooting for probes, database URLs, TLS, and app-specific failure
modes;
- backup and restore readiness gate;
diff --git a/helm/reuse-surface-values.yaml b/helm/reuse-surface-values.yaml
index d89df59..af38bc2 100644
--- a/helm/reuse-surface-values.yaml
+++ b/helm/reuse-surface-values.yaml
@@ -2,4 +2,23 @@
# REUSE_SURFACE_TOKEN is supplied via Secret reuse-surface-env.
image:
- tag: "cb7a6e4"
\ No newline at end of file
+ tag: "cb7a6e4"
+
+landing:
+ enabled: true
+ title: "REUSE federation endpoint"
+ eyebrow: "reuse.coulomb.social"
+ body: "This is the Railiance REUSE capability federation service. It is an API endpoint for automated clients and operators; there is no browser login for this service."
+ buttonLabel: "Open federated index"
+ primaryUrl: "/v1/federated"
+ links:
+ - label: "Health check"
+ url: "/health"
+ - label: "Federated capability index"
+ url: "/v1/federated"
+ - label: "Operator runbook"
+ url: "https://gitea.coulomb.social/coulomb/railiance-apps/src/branch/main/docs/reuse-surface-on-railiance01.md"
+
+ingress:
+ redirectHttp:
+ enabled: true
diff --git a/workplans/RAILIANCE-WP-0008-service-landing-pages.md b/workplans/RAILIANCE-WP-0008-service-landing-pages.md
new file mode 100644
index 0000000..78a7695
--- /dev/null
+++ b/workplans/RAILIANCE-WP-0008-service-landing-pages.md
@@ -0,0 +1,169 @@
+---
+id: RAILIANCE-WP-0008
+type: workplan
+title: "Add friendly landing pages for S5 service endpoints"
+domain: railiance
+repo: railiance-apps
+status: finished
+owner: codex
+topic_slug: railiance
+created: "2026-06-15"
+updated: "2026-06-15"
+state_hub_workstream_id: "41b9deea-a935-4372-8916-b3981336f597"
+---
+
+# Add friendly landing pages for S5 service endpoints
+
+Create a small, reusable landing-page pattern for public S5 application
+endpoints. The page should give humans a clear next step without changing the
+machine-facing API contract:
+
+- UI-backed applications may show a friendly page with a short redirect notice
+ and a visible **Continue** button to the correct login or application route.
+- API-only services should show a proper no-login informational page at `/`,
+ with links to health/status/runbook documentation where appropriate.
+
+First rollout target: **`https://reuse.coulomb.social`**, which hosts the
+`reuse-surface` federation API and intentionally has no browser login UI.
+
+## Current Context
+
+`RAILIANCE-WP-0007` deployed the `reuse-surface` Helm release and runbook for
+`reuse.coulomb.social`. The release is useful to machines and operators, but a
+human visiting `/` should not be left with an API-shaped response or a dead end.
+
+The implementation must preserve the existing CLI/API surface:
+
+- `/health` remains suitable for Kubernetes probes and external smoke checks.
+- `/v1/*` API routes continue to reach the `reuse-surface` service.
+- Authenticated federation operations continue to use `REUSE_SURFACE_TOKEN` and
+ must not expose token material in the landing page, chart values, or logs.
+
+## Design Constraints
+
+- Keep the page static and non-secret.
+- Prefer explicit Ingress path routing over changing API semantics when a
+ service has no upstream web UI.
+- Use exact `/` routing for the landing page when possible, so API prefixes are
+ unaffected.
+- Add `noindex` metadata for operator/service landing pages that are not meant
+ to be public marketing surfaces.
+- Keep the pattern reusable for later services with login destinations.
+
+---
+
+## Define Landing Page Contract
+
+```task
+id: RAILIANCE-WP-0008-T01
+status: done
+priority: high
+state_hub_task_id: "ca47efdb-ed0e-47bf-90f3-5a78c6861ebf"
+```
+
+Document the endpoint contract in `docs/s5-app-onboarding-checklist.md` or a
+small companion doc:
+
+- API-only service: static informational page at `/`, no login claim, no token
+ hints, links to health/status and operator runbook.
+- UI-backed service: landing page may auto-forward after a short delay and must
+ include a visible button to the canonical login/application route.
+- All variants must preserve health, API, OAuth callback, and asset paths.
+
+Done when future S5 app workplans can cite a single landing-page rule instead
+of rediscovering the behavior per service.
+
+## Add Reusable Helm Support
+
+```task
+id: RAILIANCE-WP-0008-T02
+status: done
+priority: high
+state_hub_task_id: "44dfe49f-a3f4-4381-a4ca-eb9218099868"
+```
+
+Add a reusable Helm pattern for a static landing page, either as shared snippets
+or as chart-local templates following one documented convention. The pattern
+should support:
+
+- enable/disable flag in values;
+- static HTML content from a ConfigMap;
+- a lightweight static HTTP container or equivalent service;
+- exact `/` Ingress path to the landing service;
+- prefix routes for API and app paths to the existing backend;
+- optional redirect target and button label for UI-backed applications.
+
+Done when `helm template` can render both disabled and enabled landing-page
+variants without changing non-landing deployments.
+
+## Implement API-Only Landing Page For reuse.coulomb.social
+
+```task
+id: RAILIANCE-WP-0008-T03
+status: done
+priority: high
+state_hub_task_id: "4e90315d-f0f0-49ca-b44f-533c23a44e96"
+```
+
+Enable the landing-page pattern in `charts/reuse-surface` and
+`helm/reuse-surface-values.yaml` for `reuse.coulomb.social`.
+
+Content should make clear that this endpoint is the Railiance REUSE capability
+federation service, not a login application. Include non-secret operator links
+or text for:
+
+- service purpose;
+- `/health`;
+- `/v1/federated`;
+- `docs/reuse-surface-on-railiance01.md`;
+- noindex metadata.
+
+Done when a browser request to `/` receives the landing page while `/health`
+and `/v1/federated` still reach the API service.
+
+## Add Login-Forward Variant For UI Applications
+
+```task
+id: RAILIANCE-WP-0008-T04
+status: done
+priority: medium
+state_hub_task_id: "80c843b2-68d0-4f8a-aba6-39db1a2a6f70"
+```
+
+Prepare the values contract and at least one documented example for services
+that do have a browser UI. The page should explain that the user is being sent
+to the application and include a visible button to the configured login or
+application route.
+
+Done when a future UI-backed app can set only values such as `redirectTarget`,
+`buttonLabel`, and explanatory copy, without editing templates.
+
+## Verify, Deploy, And Update Runbook
+
+```task
+id: RAILIANCE-WP-0008-T05
+status: done
+priority: medium
+state_hub_task_id: "0fc250a8-8fea-4c88-bd81-ad7ecf6143a1"
+```
+
+Render, deploy, and smoke-test the reuse landing page on railiance01:
+
+- `make reuse-dry-run`;
+- deploy with pinned image and non-secret landing values;
+- verify `/`, `/health`, and `/v1/federated` over the public hostname;
+- confirm no secret values appear in manifests or rendered HTML;
+- update `docs/reuse-surface-on-railiance01.md` with the landing-page behavior
+ and rollback notes.
+
+Done when the public browser experience is friendly and the existing
+machine-facing API checks still pass.
+
+Completed 2026-06-15: deployed Helm revision 5 to namespace `reuse`.
+`reuse-surface-landing` is Running, HTTP `/` redirects to HTTPS, HTTPS `/`
+returns `200 text/html`, `/health` returns `200 application/json`,
+`/v1/federated` returns `200 application/json` with 12 capabilities, and the
+fetched landing page contains no token references. Follow-up fix separated the
+API and landing Service selectors with `app.kubernetes.io/component` labels and
+changed ingress routing to explicit API paths (`/health`, `/v1`) plus landing
+fallback `/`.