{{- $replicas := include "sure.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.sure.replicas) -}}
{{- $webStartup := fromYaml (include "sure.webStartupProbe" .Values.sure.web.startupProbe) -}}
{{- $workerStartup := fromYaml (include "sure.workerStartupProbe" .Values.sure.worker.startupProbe) -}}
{{- $workerLiveness := fromYaml (include "sure.workerLivenessProbe" .Values.sure.worker.livenessProbe) -}}
{{- $httpRoute := .Values.httpRoute | default dict -}}
{{- $hostnames := $httpRoute.hostnames | default list -}}
{{- if not $hostnames -}}
{{- $hostnames = list .Values.ingress.domain -}}
{{- end -}}
Sure {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.

{{- if .Values.ingress.enabled }}

ACCESS — INGRESS MODE (ingress.enabled: true, httpRoute.enabled: false)
  https://{{ .Values.ingress.domain }}

  The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}"
  cluster issuer. Until it has been issued the browser shows a warning; follow it with

    kubectl -n {{ .Release.Namespace }} get certificate tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress

  The first account created on a fresh instance becomes the super admin — sign
  in yourself before handing the URL to anyone.
{{- else if $httpRoute.enabled }}

ACCESS — GATEWAY MODE (httpRoute.enabled: true, ingress.enabled: false)
{{- range $hostnames }}
  https://{{ . }}
{{- end }}

  The first account created on a fresh instance becomes the super admin — sign
  in yourself before handing the URL to anyone.

  This release renders an HTTPRoute ({{ .Release.Name }}-{{ .Chart.Name }}-httproute) and NOTHING else
  of the routing path — no Gateway, no class, no Certificate. Three things
  therefore belong to the cluster and not to this chart:

    1. The Gateway-API CRDs. Without them this install would have failed.
    2. The Gateway named in httpRoute.parentRefs
{{- range $httpRoute.parentRefs }}
         {{ .name }}{{ if .namespace }} in namespace {{ .namespace }}{{ end }}{{ if .sectionName }}, listener {{ .sectionName }}{{ end }}
{{- end }}
       must exist AND its listener must allow routes from this namespace
       ({{ .Release.Namespace }}) in allowedRoutes. A rejected attachment shows
       up only in the route's status:

         kubectl -n {{ .Release.Namespace }} get httproute {{ .Release.Name }}-{{ .Chart.Name }}-httproute -o yaml

       Read status.parents[].conditions — "Accepted: True" and
       "ResolvedRefs: True" is a working attachment. An empty status means no
       controller has looked at the route at all.
    3. The TLS certificate for the hostname, on the Gateway's listener. This is
       the one to get right here: the chart renders RAILS_ASSUME_SSL and
       RAILS_FORCE_SSL as "true" in this mode, i.e. Rails TRUSTS the forwarded
       scheme, sets secure cookies and sends HSTS. A Gateway listener that is
       plain HTTP would therefore hand out cookies the browser never returns.
       ingress.clusterIssuer is NOT used and not required — the chart renders
       no Certificate, so there is no tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress secret.

  APP_DOMAIN, WEBAUTHN_RP_ID and WEBAUTHN_ALLOWED_ORIGINS still follow
  ingress.domain ({{ .Values.ingress.domain }}), not httpRoute.hostnames. Passkeys are bound to
  the relying party id, so publishing under a name that ingress.domain does not
  carry gives you passkeys registered against the wrong party — and changing
  ingress.domain afterwards invalidates every credential already registered.

  ingress.annotations is inert in this mode: the Gateway API has no annotation
  channel. Nothing this chart needs lives there.
{{- else }}

ACCESS — NEITHER INGRESS NOR HTTPROUTE
  ingress.enabled and httpRoute.enabled are both false, so this release has no
  Ingress, no HTTPRoute and no certificate.
  ingress.domain is still required: it becomes APP_DOMAIN (the host in every
  mail link) and the WebAuthn relying party the app binds passkeys to. Reach
  the instance inside the cluster with

    kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80

  and open http://localhost:8080. With nothing published the chart also renders
  RAILS_ASSUME_SSL and RAILS_FORCE_SSL as "false", which is what makes that
  plain-HTTP session able to log in at all; mail links still point at
  https://{{ .Values.ingress.domain }}/.
{{- end }}
{{ if eq $replicas "0" }}
SCALED TO ZERO
  {{ if .Values.scaleDown }}scaleDown: true{{ else }}sure.replicas: 0{{ end }} — the StatefulSet runs with replicas: 0, so neither the web nor
  the worker process is running. PVC, Secrets, Service{{ if .Values.ingress.enabled }}, Ingress and the
  certificate stay{{ else if $httpRoute.enabled }}, and the HTTPRoute stay{{ else }} stay{{ end }} in place; set it back and upgrade to bring both containers up.
{{ end }}
WHAT THE THREE 1PASSWORD ITEMS MUST CONTAIN
  The chart creates OnePasswordItem CRs; the operator turns each into a
  Kubernetes Secret whose keys are the FIELD NAMES of the 1Password item. A
  missing field does not fail the install — the release goes through and the
  pod then hangs in CreateContainerConfigError naming the key it could not
  find (kubectl -n {{ .Release.Namespace }} describe pod).

  secrets.appSecretRef → {{ .Values.secrets.appSecretRef }}
    Secret {{ .Release.Name }}-{{ .Chart.Name }}-secret, field:
      secret-key-base         SECRET_KEY_BASE, 64 random bytes hex
                              (openssl rand -hex 64)

  secrets.dbSecretRef → {{ .Values.secrets.dbSecretRef }}
    Secret {{ .Release.Name }}-{{ .Chart.Name }}-db-secret, fields:
      host                    DB_HOST, the PostgreSQL hostname
      port                    DB_PORT, usually 5432
      database                POSTGRES_DB
      username                POSTGRES_USER
      password                POSTGRES_PASSWORD

  secrets.redisSecretRef → {{ .Values.secrets.redisSecretRef }}
    Secret {{ .Release.Name }}-{{ .Chart.Name }}-redis-secret, field:
      connection-url          REDIS_URL, e.g. redis://:password@host:6379/0

ROTATING SECRET_KEY_BASE DESTROYS MORE THAN SESSIONS
  In self-hosted mode Sure derives its Active Record encryption keys from
  SECRET_KEY_BASE (config/initializers/active_record_encryption.rb: three
  SHA-256 digests over that one value) unless
  ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY and its two companions are set. Every
  encrypted column in the database — provider tokens, OAuth credentials, MFA
  secrets — is therefore readable only with the SECRET_KEY_BASE it was written
  under. Replacing that value does not just log everyone out: it makes the
  existing encrypted data undecryptable, and no rotation of the item brings it
  back. Treat this field as permanent for the life of the database, or pin the
  three encryption keys explicitly through sure.web.env / sure.worker.env
  before the first start.

CHANGING A SECRET'S CONTENT RESTARTS NOTHING
  Helm never sees the content of a OnePasswordItem, so it cannot hash it. The
  pod template carries checksum/secret-ref annotations over the rendered
  OnePasswordItems, so pointing a *SecretRef at a DIFFERENT item rolls the
  pod — but editing a value INSIDE the same item does not: the operator
  updates the Kubernetes Secret while both containers keep the old value in
  their environment. After such a change:

    kubectl -n {{ .Release.Namespace }} rollout restart statefulset/{{ .Release.Name }}-{{ .Chart.Name }}-sfs

  The operator's auto-restart annotation would not help here and is
  deliberately not set: it restarts Deployments only, and this is a
  StatefulSet.

POSTGRES AND REDIS ARE NOT PART OF THIS CHART
  Both must exist and be reachable before the first start. Redis is not a
  cache the app can do without — it holds the Sidekiq queues, the Rails cache
  store and the rate limiter.

  What the probes do with that, per container:
    web     liveness and startup use /up, which is Rails' boot check and
            touches no database — a Postgres outage therefore does not restart
            the pod. Readiness uses "/", which runs a User.count query, so the
            pod leaves the Service while the database is gone and returns on
            its own afterwards.
    worker  all three probes run "sidekiqmon processes" and look for this
            pod's own entry, i.e. a live Sidekiq heartbeat in Redis. There is
            no HTTP and no dependency-free alternative: with Redis gone the
            worker has nothing to do, and liveness only restarts after
            {{ $workerLiveness.failureThreshold }} x {{ $workerLiveness.periodSeconds }}s = {{ mul $workerLiveness.failureThreshold $workerLiveness.periodSeconds }}s of missing heartbeat.

  A pod that never turns Ready is usually a connection problem, not a chart
  problem: read the container logs, not the pod events.

    kubectl -n {{ .Release.Namespace }} logs statefulset/{{ .Release.Name }}-{{ .Chart.Name }}-sfs -c {{ .Release.Name }}-{{ .Chart.Name }}-web

THE FIRST START MIGRATES, AND THE STARTUP PROBE IS THE CLOCK
  The image entrypoint runs "./bin/rails db:prepare" — but only when the
  command is "./bin/rails server". That is the web container, and only the web
  container: the worker runs "bundle exec sidekiq" and never migrates, so the
  two can never race. Puma binds the port after the migration, which means the
  whole schema creation happens inside the startup probe's budget:

    web     {{ $webStartup.failureThreshold }} x {{ $webStartup.periodSeconds }}s = {{ mul $webStartup.failureThreshold $webStartup.periodSeconds }}s
    worker  {{ $workerStartup.failureThreshold }} x {{ $workerStartup.periodSeconds }}s = {{ mul $workerStartup.failureThreshold $workerStartup.periodSeconds }}s (it waits for the schema the web container creates)

  Raise sure.web.startupProbe.failureThreshold BEFORE a large app-version jump
  or a first import, not after the crash loop has started — a restart lands in
  the middle of a migration. Back up the database before any app-version bump:
  migrations are one-way, and a helm rollback returns the old image to a
  database the new one has already rewritten.

ONE POD, ONE VOLUME, TWO CONTAINERS
  Uploads, imports and exports live on the ReadWriteOnce PVC
  {{ .Release.Name }}-{{ .Chart.Name }}-pvc, mounted at /rails/storage in both containers. That is
  the reason web and worker share a pod at all: a background job writes the
  same files the web process serves, and one RWO volume cannot be attached to
  two pods.
{{- if gt (int $replicas) 1 }}

  sure.replicas is {{ $replicas }}. Expect the extra pods to stay Pending on
  "Multi-Attach error for volume" unless the storage class is genuinely RWX.
{{- end }}

  Backup is two things at the same time: the database holds the accounts and
  one row per attachment, the PVC holds the bytes. Restoring one without the
  other gives an app full of broken references. Because the PVC is rendered by
  the chart, helm uninstall deletes it — the external database survives that,
  which makes an accidental uninstall look recoverable while the files are
  already gone.

THE CHART CONFIGURES NO MAIL AND NO AI
  Sure sends invitations and notifications over SMTP and can run an assistant
  against an OpenAI-compatible endpoint. This chart sets neither: no SMTP_*
  variable, no OPENAI_* variable. Mails are therefore quietly never sent, and
  the AI features stay off — nothing in the UI says so.

  Both are plain environment variables, so they go into sure.web.env and, for
  anything a background job needs (the assistant runs in the worker), into
  sure.worker.env as well. The secretKeyRef form is available for the API key
  and the SMTP password. Note that upstream sizes AI usage at 1 GB of memory
  per process; the chart's computed memory limit is 3x its request.
{{- if or .Values.sure.web.env .Values.sure.worker.env }}

  Currently set: {{ len .Values.sure.web.env }} extra variable(s) on the web container,
  {{ len .Values.sure.worker.env }} on the worker, rendered after the ones the chart sets itself.
  An entry repeating a name the chart already sets wins at install time and is
  silently lost on the next helm upgrade — read it back off the StatefulSet.
{{- end }}
