# Felhom Hub — Multi-customer dashboard
# Dashboard: https://hub.felhom.eu
# API: POST /api/v1/report (Bearer token auth)
#
# Receives health reports from customer controllers and displays
# a centralized overview dashboard for the operator (Viktor).
#
# Namespace: felhom-system (shared with healthchecks and other felhom infra)
#
# PREREQUISITES:
#   1. Build and push the hub image:
#        cd ~/build/felhom-hub && ./build.sh v0.2.0 --push
#
#   2. Generate a bcrypt password hash for dashboard login:
#        htpasswd -nbBC 10 "" "your-password" | cut -d: -f2
#      Update the ConfigMap password_hash field below.
#
#   3. Create the operator/global bearer key Secret (out-of-band, NEVER committed):
#        openssl rand -hex 32   # mint
#        kubectl -n felhom-system create secret generic report-api \
#          --from-literal=REPORT_API_KEY=<minted-key>
#      (Customer boxes use per-customer/per-host keys generated by the hub — the global
#       key is the operator's own, e.g. felhom-ops -hub-key.)
#
#   4. Apply this manifest:
#        kubectl apply -f manifests/hub.yaml
#
#   5. Configure DNS:
#      Add hub.felhom.eu → k3s cluster IP in Cloudflare
#
# DEBUGGING:
#   kubectl logs -n felhom-system deploy/hub -f
#   kubectl exec -it -n felhom-system deploy/hub -- ls /data/
#   kubectl describe ingress -n felhom-system hub

# =============================================================================
# PERSISTENT STORAGE
# =============================================================================
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: hub-data
  namespace: felhom-system
  labels:
    app: hub
    recurring-job-group.longhorn.io/default: disabled
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: longhorn
  resources:
    requests:
      storage: 1Gi

# =============================================================================
# CONFIGURATION
# =============================================================================
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: hub-config
  namespace: felhom-system
data:
  hub.yaml: |
    auth:
      # Bcrypt hash for dashboard login (Viktor only)
      # Generate: htpasswd -nbBC 10 "" "your-password" | cut -d: -f2
      password_hash: "$2y$10$N5.O9jBnc.1tIlJT/irx3OlVjJQemlCHRnfqIJg/EyZofnzXSCpeG"
    api:
      # Operator/global bearer key. NOT stored here since v0.53.0 — injected at runtime from
      # Secret/report-api via the REPORT_API_KEY env var (see Deployment below). The Secret is
      # created out-of-band and NOT committed (documentation/runbooks/secrets.md); the previously
      # committed literal is retired by ROTATION (see the publish-runbook notes). Leave empty.
      report_api_key: ""
    retention:
      max_days: 90
      prune_schedule: "04:30"
    alerting:
      stale_threshold: "30m"
    notifications:
      # Resend API key is NOT stored here. It is injected at runtime from Secret/resend-api
      # via the RESEND_API_KEY env var (see Deployment below). The Secret is created out-of-band
      # and is NOT committed — see documentation/runbooks/secrets.md. Leave this empty.
      resend_api_key: ""
      # Operator alert recipient + enable. WITHOUT both, Dispatcher.processOperator returns early and
      # NO operator email is ever sent — the self-health pipeline (probe→report→checker→dispatch) stops
      # one hop short of the inbox (TESTRUN finding: the unproven hop). The address is the operator's own
      # and is not a secret. from_email defaults to monitoring@felhom.eu.
      operator_email: "admin@felhom.eu"
      operator_enabled: true
    registry:
      image: "gitea.dooplex.hu/admin/felhom-controller"
      # username + token injected via REGISTRY_USERNAME / REGISTRY_TOKEN env vars
      # from Secret/gitea-creds (see Deployment below)
      check_interval: "6h"
      template_interval: "1h"
    server:
      listen: ":8080"
      data_dir: "/data"

# =============================================================================
# DEPLOYMENT
# =============================================================================
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hub
  namespace: felhom-system
  labels:
    app: hub
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: hub
  template:
    metadata:
      labels:
        app: hub
    spec:
      containers:
        - name: hub
          image: gitea.dooplex.hu/admin/felhom-hub:0.94.0
          ports:
            - containerPort: 8080
              name: http
          env:
            - name: TZ
              value: "Europe/Budapest"
            # Phase 2 managed updates: global controller-version FLOOR fallback. Any reporting box below
            # this auto-updates to it (unless a per-customer override is set via the operator UI).
            - name: DEFAULT_MIN_CONTROLLER_VERSION
              value: "0.120.0"
            # Resend API key — injected from the out-of-band Secret/resend-api (NOT committed).
            # See documentation/runbooks/secrets.md. Overrides the empty ConfigMap placeholder.
            - name: RESEND_API_KEY
              valueFrom:
                secretKeyRef:
                  name: resend-api
                  key: RESEND_API_KEY
            # Operator/global bearer key — injected from the out-of-band Secret/report-api
            # (NOT committed; documentation/runbooks/secrets.md). Deliberately NOT optional:
            # a missing Secret must fail the pod Ready rather than boot an unauthenticatable
            # hub with an empty bearer key. Create the Secret BEFORE syncing this manifest.
            - name: REPORT_API_KEY
              valueFrom:
                secretKeyRef:
                  name: report-api
                  key: REPORT_API_KEY
            - name: REGISTRY_USERNAME
              valueFrom:
                secretKeyRef:
                  name: gitea-creds
                  key: username
            - name: REGISTRY_TOKEN
              valueFrom:
                secretKeyRef:
                  name: gitea-creds
                  key: password
            # S1 offsite connectivity: the WG peer-sync push channel (doc 06 §5 + runbook
            # offsite-endpoint.md). Addr is dev-phase literal (the throwaway endpoint); the SSH
            # private key + (non-secret) pinned host key come from Secret/wg-endpoint-ssh,
            # created out-of-band in runbook step 6 — optional so the pod starts before it
            # exists (the hub logs peer-sync disabled until then).
            - name: WG_ENDPOINT_SSH_ADDR
              value: "167.233.158.164:22"
            - name: WG_ENDPOINT_SSH_USER
              value: "felhom-peersync"
            - name: WG_ENDPOINT_SSH_KEY_FILE
              value: "/etc/hub-secrets/wg-endpoint-ssh/key"
            - name: WG_ENDPOINT_SSH_HOSTKEY
              valueFrom:
                secretKeyRef:
                  name: wg-endpoint-ssh
                  key: hostkey
                  optional: true
            # PBS DR tier (SLICE 1): the tenantsync channel — same endpoint + pinned host key as
            # peersync (env above), its OWN private key from Secret/tenantsync (out-of-band,
            # runbook offsite-endpoint.md §10). Optional: absent → the hub logs tenantsync disabled.
            - name: TENANTSYNC_SSH_KEY_FILE
              value: "/etc/hub-secrets/tenantsync/key"
            # Agent-plane immediate-sync (Direction-2a, v0.59.0): the poke sender — same endpoint +
            # pinned host key + peersync user as above, its OWN forced-command key from Secret/agent-poke
            # (out-of-band; the ep0 authorized_keys line carries the PUBLIC half, command="felhom-poke").
            # Optional: absent → the hub logs the poke disabled; agent-plane saves still reconcile in
            # ≤15 min. See documentation/runbooks/offsite-endpoint.md (poke section).
            - name: POKE_SSH_KEY_FILE
              value: "/etc/hub-secrets/agent-poke/key"
            # Offsite provisioning (SLICE 1+2): Hetzner Storage Box API token + the NUMERIC id of the
            # pool box, from the out-of-band Secret/storagebox (NOT committed). The token MUST be scoped
            # to the dedicated storage project — NEVER the shared-project token (it can touch ep0).
            # HETZNER_POOL_BOX_ID is the numeric box id (console #id), not the box name. Optional so the
            # pod starts before the secret exists (hub then logs offsite provisioning disabled).
            - name: HETZNER_POOL_BOX_ID
              valueFrom:
                secretKeyRef:
                  name: storagebox
                  key: HETZNER_POOL_BOX_ID
                  optional: true
            - name: HETZNER_TOKEN
              valueFrom:
                secretKeyRef:
                  name: storagebox
                  key: HETZNER_TOKEN
                  optional: true
            # Non-secret; the hub defaults to fsn1 anyway — explicit for clarity.
            - name: HETZNER_LOCATION
              value: "fsn1"
          resources:
            requests:
              memory: "64Mi"
              cpu: "50m"
            limits:
              memory: "256Mi"
              cpu: "500m"
          volumeMounts:
            - name: data
              mountPath: /data
            - name: config
              mountPath: /etc/felhom-hub
            - name: wg-endpoint-ssh
              mountPath: /etc/hub-secrets/wg-endpoint-ssh
              readOnly: true
            - name: tenantsync
              mountPath: /etc/hub-secrets/tenantsync
              readOnly: true
            - name: agent-poke
              mountPath: /etc/hub-secrets/agent-poke
              readOnly: true
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 30
            timeoutSeconds: 5
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 3
            periodSeconds: 10
            timeoutSeconds: 3
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: hub-data
        - name: config
          configMap:
            name: hub-config
        - name: wg-endpoint-ssh
          secret:
            secretName: wg-endpoint-ssh
            optional: true
            items:
              - key: key
                path: key
                mode: 0400
        - name: tenantsync
          secret:
            secretName: tenantsync
            optional: true
            items:
              - key: key
                path: key
                mode: 0400
        - name: agent-poke
          secret:
            secretName: agent-poke
            optional: true
            items:
              - key: key
                path: key
                mode: 0400

# =============================================================================
# SERVICE
# =============================================================================
---
apiVersion: v1
kind: Service
metadata:
  name: hub
  namespace: felhom-system
  labels:
    app: hub
spec:
  selector:
    app: hub
  ports:
    - port: 8080
      targetPort: 8080
      name: http

# =============================================================================
# INGRESS — hub.felhom.eu
# =============================================================================
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: hub
  namespace: felhom-system
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/proxy-body-size: "2m"
    # Geo-restrict to Hungary (operator-only dashboard)
    # NOTE: /api/v1/report must also be reachable — all customers are in HU
    nginx.ingress.kubernetes.io/configuration-snippet: |
      set $geo_allowed 0;
      if ($remote_addr ~ "^192\.168\.") { set $geo_allowed 1; }
      if ($remote_addr ~ "^10\.") { set $geo_allowed 1; }
      if ($geoip2_country_code = "HU") { set $geo_allowed 1; }
      if ($geo_allowed = 0) {
        return 403 "Access restricted to Hungary";
      }
spec:
  ingressClassName: nginx-internal
  tls:
    - hosts:
        - hub.felhom.eu
      secretName: hub-felhom-eu-tls
  rules:
    - host: hub.felhom.eu
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: hub
                port:
                  number: 8080