# FlowerCore Chat # # ArgoCD-managed workload plus TLS/Ingress. The chat-web-secret remains an # out-of-band Secret until the values are moved into a 1Password-backed item; # the Deployment references it as optional so GitOps can own the workload # without storing secret material in this repo. --- apiVersion: v1 kind: Namespace metadata: name: fc-chat labels: app.kubernetes.io/part-of: flowercore --- apiVersion: v1 kind: ConfigMap metadata: name: chat-web-config namespace: fc-chat labels: app.kubernetes.io/name: chat-web app.kubernetes.io/part-of: flowercore data: ASPNETCORE_ENVIRONMENT: Production ASPNETCORE_URLS: "http://+:8080" ASPNETCORE_FORWARDEDHEADERS_ENABLED: "true" FlowerCore__Auth__Enabled: "false" FlowerCore__Auth__Oidc__Enabled: "true" FlowerCore__Auth__Oidc__Authority: "https://id.iamworkin.lan/application/o/chat/" FlowerCore__Auth__Oidc__Audience: "chat" FlowerCore__Auth__Oidc__ClientId: "chat" FlowerCore__Database__ConnectionStrings__Sqlite: "Data Source=/data/chat.db" # Ollama target. Switched 2026-04-25 from edge1 Pi5 (10.0.57.17) to BLUEJAY-WS # workstation (10.0.56.20, RX 9070 XT 16GB, OLLAMA_HOST=0.0.0.0:11434, Vulkan # backend per feedback_rdna4_vulkan_broken). The Pi5 was timing out every team- # round speaker at the 300s per-turn cap (live-proven 2026-04-25 03:53 UTC, # see feedback_chat_team_round_edge1_too_slow). Workstation has gemma3:4b for # the Cheap tier, plus gemma3:27b/phi4:14b/qwen3:14b for Default/Balanced/Deep. # Piper TTS stays on edge1 below (different service, Pi handles TTS fine). FlowerCore__AI__OllamaBaseUrl: "http://10.0.56.20:11434" FlowerCore__AI__DefaultModelName: "phi4:14b" ChatOptions__BehaviorRuleEngine__OllamaBaseUrl: "http://10.0.56.20:11434" ChatOptions__BehaviorRuleEngine__FallbackOllamaBaseUrl: "http://10.0.57.17:11434" ChatOptions__BehaviorRuleEngine__ModelName: "gemma3:12b" FlowerCore__AI__Memory__UseSharedIndexingAdapter: "true" FlowerCore__AI__Memory__UseOllamaEmbeddings: "true" FlowerCore__AI__Memory__EmbeddingModel: "nomic-embed-text" FlowerCore__AI__Memory__EnableSharedIndexingBackfill: "true" FlowerCore__AI__Memory__SharedIndexingDatabasePath: "/data/chat-memory-index.db" FlowerCore__AI__Skills__Library__LibraryApiUrl: "http://library-web.fc-library.svc.cluster.local" FlowerCore__AI__Skills__Retail__RetailApiUrl: "http://retail-web.fc-retail.svc.cluster.local" FlowerCore__AI__Skills__Intranet__IntranetBaseUrl: "http://intranet-web.intranet.svc.cluster.local" FlowerCore__AI__Skills__Print__PrintMcpBaseUrl: "http://10.0.57.16:5200" FlowerCore__AI__IrcBridge__Enabled: "true" FlowerCore__AI__IrcBridge__DefaultProfileSlug: "it-helpdesk" FlowerCore__AI__IrcBridge__MentionProfileSlug: "it-helpdesk" FlowerCore__AI__IrcBridge__MentionReactiveMode: "mentions-only" FlowerCore__AI__IrcBridge__AllowActionExecution: "false" FlowerCore__AI__Voice__Piper__Host: "10.0.57.17" FlowerCore__AI__Voice__Piper__Port: "10400" FlowerCore__AI__Voice__OutputRoot: "/data/audio" FlowerCore__AI__Voice__RetentionDays: "30" # LLM provider abstraction (ADR-088). Anthropic stays disabled here -- when # an operator wants to enable Claude, they flip Enabled=true and mount # FlowerCore__Anthropic__ApiKey from the onepassword-synced Secret (see # docs/ai-agents/anthropic-integration.md). FlowerCore__Anthropic__Enabled: "false" FlowerCore__Anthropic__BaseUrl: "https://api.anthropic.com" FlowerCore__Anthropic__DefaultModel: "claude-sonnet-4-6" FlowerCore__Anthropic__CheapModel: "claude-haiku-4-5-20251001" FlowerCore__Anthropic__DeepModel: "claude-opus-4-7" FlowerCore__Budget__ResponseCacheEnabled: "true" OTEL_SERVICE_NAME: FlowerCore.Chat OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.monitoring.svc.cluster.local:4317" OTEL_EXPORTER_OTLP_PROTOCOL: grpc --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: chat-web-data namespace: fc-chat labels: app.kubernetes.io/name: chat-web app.kubernetes.io/part-of: flowercore spec: accessModes: - ReadWriteOnce storageClassName: longhorn volumeMode: Filesystem resources: requests: storage: 1Gi --- apiVersion: apps/v1 kind: Deployment metadata: name: chat-web namespace: fc-chat labels: app.kubernetes.io/name: chat-web app.kubernetes.io/part-of: flowercore spec: replicas: 1 strategy: type: Recreate selector: matchLabels: app.kubernetes.io/name: chat-web template: metadata: labels: app.kubernetes.io/name: chat-web app.kubernetes.io/part-of: flowercore annotations: prometheus.io/scrape: "true" prometheus.io/port: "8080" prometheus.io/path: "/metrics/prometheus" spec: nodeSelector: kubernetes.io/hostname: rke2-server securityContext: fsGroup: 1654 fsGroupChangePolicy: OnRootMismatch containers: - name: chat-web image: localhost/fc-chat-web:v20260603-oidc-authentik imagePullPolicy: Never ports: - name: http containerPort: 8080 envFrom: - configMapRef: name: chat-web-config - secretRef: name: chat-web-secret optional: true env: - name: FlowerCore__Auth__Oidc__Authority valueFrom: secretKeyRef: name: chat-oidc-client key: issuer_url optional: true - name: FlowerCore__Auth__Oidc__ClientId valueFrom: secretKeyRef: name: chat-oidc-client key: client_id optional: true - name: FlowerCore__Auth__Oidc__ClientSecret valueFrom: secretKeyRef: name: chat-oidc-client key: client_secret optional: true volumeMounts: - name: data mountPath: /data resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m" readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 6 livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 30 periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 3 volumes: - name: data persistentVolumeClaim: claimName: chat-web-data --- apiVersion: v1 kind: Service metadata: name: chat-web namespace: fc-chat labels: app.kubernetes.io/name: chat-web app.kubernetes.io/part-of: flowercore spec: type: ClusterIP selector: app.kubernetes.io/name: chat-web ports: - name: http port: 80 targetPort: 8080 protocol: TCP --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: chat-web-tls namespace: fc-chat spec: secretName: chat-web-tls issuerRef: name: step-ca-acme kind: ClusterIssuer dnsNames: - chat.iamworkin.lan --- apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: name: chat-web namespace: fc-chat spec: entryPoints: - websecure routes: - match: Host(`chat.iamworkin.lan`) kind: Rule services: - name: chat-web port: 80 tls: secretName: chat-web-tls --- # Public host profile marker. The app treats this header as authoritative for # the public twin, while the internal chat.iamworkin.lan route does not attach # it and keeps the operator-oriented UI. apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: chat-public-profile-header namespace: fc-chat spec: headers: customRequestHeaders: X-FC-Chat-Host-Profile: "public" --- # Public Cloudflare-fronted twin for the anonymous chat surface. Operator # paths are intentionally absent from the allowlist below, so /admin, # /operator, /console, /ops, /api/operator, and /operatorhub miss this route # and return Traefik 404 before reaching the pod. Operator action still needed: # create/verify Cloudflare DNS chat.flowercore.io -> public Traefik endpoint # and mirror the cf-origin-flowercore-io TLS secret into namespace fc-chat. apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: name: chat-web-public namespace: fc-chat spec: entryPoints: - websecure routes: - match: Host(`chat.flowercore.io`) && (Path(`/`) || Path(`/chat`) || PathPrefix(`/_blazor`) || PathPrefix(`/_framework`) || PathPrefix(`/_content`) || PathPrefix(`/avatars`) || PathPrefix(`/css`) || PathPrefix(`/js`) || PathPrefix(`/favicon`) || PathPrefix(`/chathub`)) && (Method(`GET`) || Method(`HEAD`) || Method(`POST`) || Method(`OPTIONS`)) kind: Rule middlewares: - name: chat-public-profile-header services: - name: chat-web port: 80 tls: secretName: cf-origin-flowercore-io