feat(family): deploy Single-Writer DuckDB backend, Mila parent cockpit & feeding analytics, and Moritz macro tracker

This commit is contained in:
Moritz Graf
2026-10-17 07:21:49 +02:00
parent ef50d87bd7
commit 5b8432caf4
96 changed files with 16409 additions and 29 deletions
+151
View File
@@ -0,0 +1,151 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: duckdb-backend
namespace: family
labels:
app.kubernetes.io/name: duckdb-backend
app.kubernetes.io/part-of: family-portal
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: duckdb-backend
template:
metadata:
labels:
app.kubernetes.io/name: duckdb-backend
app.kubernetes.io/part-of: family-portal
spec:
imagePullSecrets:
- name: registry-haumdaucher-de
initContainers:
- name: git-init
image: alpine/git:latest
command: ["/bin/sh", "-c"]
args:
- |
set -e
export HOME=/tmp
cd /wiki
if [ ! -d ".git" ]; then
echo "Initializing wiki repository on dedicated PVC..."
REPO_URL="https://${GIT_SYNC_USERNAME}:${GIT_SYNC_PASSWORD}@git.moritzgraf.de/moritz/knowledge-wiki.git"
git config --global http.sslVerify false
git config --global --add safe.directory /wiki
git config --global --add safe.directory '*'
git init
git remote add origin "$REPO_URL" || git remote set-url origin "$REPO_URL"
git fetch origin main
git checkout -f -B main origin/main
echo "Clone completed successfully."
else
echo "Repository already initialized on PVC, updating..."
REPO_URL="https://${GIT_SYNC_USERNAME}:${GIT_SYNC_PASSWORD}@git.moritzgraf.de/moritz/knowledge-wiki.git"
git config --global http.sslVerify false
git config --global --add safe.directory /wiki
git config --global --add safe.directory '*'
git remote set-url origin "$REPO_URL"
if [ -f .git/shallow ]; then
echo "Unshallowing existing repository..."
git fetch --unshallow origin main || git fetch origin main
else
git fetch origin main
fi
git pull --rebase --autostash origin main || true
fi
env:
- name: GIT_SYNC_USERNAME
valueFrom:
secretKeyRef:
name: wiki-git-credentials
key: git-sync-username
- name: GIT_SYNC_PASSWORD
valueFrom:
secretKeyRef:
name: wiki-git-credentials
key: wiki-sync-token
volumeMounts:
- name: duckdb-wiki-data
mountPath: /wiki
containers:
- name: duckdb-backend
image: registry.haumdaucher.de/family/duckdb-backend:latest
imagePullPolicy: Always
env:
- name: DATA_DIR
value: "/wiki/data"
- name: DUCKDB_DATA_DIR
value: "/wiki/data"
- name: WIKI_DIR
value: "/wiki"
- name: GIT_REPO_DIR
value: "/wiki"
- name: GIT_REPO_URL
value: "https://git.moritzgraf.de/moritz/knowledge-wiki.git"
- name: GIT_REMOTE_URL
value: "https://git.moritzgraf.de/moritz/knowledge-wiki.git"
- name: GIT_SYNC_USERNAME
valueFrom:
secretKeyRef:
name: wiki-git-credentials
key: git-sync-username
- name: GIT_SYNC_PASSWORD
valueFrom:
secretKeyRef:
name: wiki-git-credentials
key: wiki-sync-token
- name: GIT_SYNC_INTERVAL_SECONDS
value: "60"
- name: PORT
value: "8000"
- name: HOST
value: "0.0.0.0"
ports:
- name: http
containerPort: 8000
volumeMounts:
- name: duckdb-wiki-data
mountPath: /wiki
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 1000m
memory: 1Gi
readinessProbe:
httpGet:
path: /duckdb-api/v1/healthz
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /duckdb-api/v1/healthz
port: 8000
initialDelaySeconds: 15
periodSeconds: 20
volumes:
- name: duckdb-wiki-data
persistentVolumeClaim:
claimName: duckdb-wiki-data
---
apiVersion: v1
kind: Service
metadata:
name: duckdb-backend
namespace: family
labels:
app.kubernetes.io/name: duckdb-backend
app.kubernetes.io/part-of: family-portal
spec:
type: ClusterIP
ports:
- port: 8000
targetPort: 8000
name: http
selector:
app.kubernetes.io/name: duckdb-backend
Binary file not shown.
+55
View File
@@ -0,0 +1,55 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: family-portal
namespace: family
annotations:
kubernetes.io/ingress.class: "traefik"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
kubernetes.io/tls-acme: "true"
traefik.ingress.kubernetes.io/router.middlewares: auth-authentik-forwardauth@kubernetescrd
spec:
ingressClassName: traefik
tls:
- hosts:
- family.haumdaucher.de
secretName: family-haumdaucher-de-tls
rules:
- host: family.haumdaucher.de
http:
paths:
- path: /duckdb-api
pathType: Prefix
backend:
service:
name: duckdb-backend
port:
number: 8000
- path: /tracking/mila
pathType: Prefix
backend:
service:
name: mila
port:
number: 80
- path: /tracking/moritz
pathType: Prefix
backend:
service:
name: moritz
port:
number: 80
- path: /outpost.goauthentik.io
pathType: Prefix
backend:
service:
name: duckdb-backend
port:
number: 8000
- path: /
pathType: Prefix
backend:
service:
name: mila
port:
number: 80
+64
View File
@@ -0,0 +1,64 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mila
namespace: family
labels:
app.kubernetes.io/name: mila
app.kubernetes.io/part-of: family-portal
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: mila
template:
metadata:
labels:
app.kubernetes.io/name: mila
app.kubernetes.io/part-of: family-portal
spec:
imagePullSecrets:
- name: registry-haumdaucher-de
containers:
- name: mila
image: registry.haumdaucher.de/family/mila:latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 80
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 100m
memory: 128Mi
readinessProbe:
httpGet:
path: /tracking/mila/
port: 80
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /tracking/mila/
port: 80
initialDelaySeconds: 10
periodSeconds: 15
---
apiVersion: v1
kind: Service
metadata:
name: mila
namespace: family
labels:
app.kubernetes.io/name: mila
app.kubernetes.io/part-of: family-portal
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
name: http
selector:
app.kubernetes.io/name: mila
+64
View File
@@ -0,0 +1,64 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: moritz
namespace: family
labels:
app.kubernetes.io/name: moritz
app.kubernetes.io/part-of: family-portal
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: moritz
template:
metadata:
labels:
app.kubernetes.io/name: moritz
app.kubernetes.io/part-of: family-portal
spec:
imagePullSecrets:
- name: registry-haumdaucher-de
containers:
- name: moritz
image: registry.haumdaucher.de/family/moritz:latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 80
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 100m
memory: 128Mi
readinessProbe:
httpGet:
path: /tracking/moritz/
port: 80
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /tracking/moritz/
port: 80
initialDelaySeconds: 10
periodSeconds: 15
---
apiVersion: v1
kind: Service
metadata:
name: moritz
namespace: family
labels:
app.kubernetes.io/name: moritz
app.kubernetes.io/part-of: family-portal
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
name: http
selector:
app.kubernetes.io/name: moritz
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: family
labels:
name: family
+15
View File
@@ -0,0 +1,15 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: duckdb-wiki-data
namespace: family
labels:
app.kubernetes.io/name: duckdb-backend
app.kubernetes.io/part-of: family-portal
spec:
accessModes:
- ReadWriteOnce
storageClassName: openebs-hostpath
resources:
requests:
storage: 5Gi
Binary file not shown.
+30 -9
View File
@@ -10,15 +10,37 @@ spec:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: hosting-legacy-redirect
name: hosting-redirect-mila
namespace: minio
spec:
redirectRegex:
regex: "^https?://([^/]+)/(mila|moritz|nutrition|sleep)\\.html(.*)"
replacement: "https://${1}/family/${2}.html${3}"
permanent: false
regex: "^https?://([^/]+)/(?:family/)?(mila|sleep)\\.html.*"
replacement: "https://family.haumdaucher.de/tracking/mila/"
permanent: true
---
# Public Ingress for hosting.haumdaucher.de (Recipes, Public Assets)
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: hosting-redirect-moritz
namespace: minio
spec:
redirectRegex:
regex: "^https?://([^/]+)/(?:family/)?(moritz|nutrition)\\.html.*"
replacement: "https://family.haumdaucher.de/tracking/moritz/"
permanent: true
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: hosting-redirect-family-root
namespace: minio
spec:
redirectRegex:
regex: "^https?://([^/]+)/family/?$"
replacement: "https://family.haumdaucher.de/"
permanent: true
---
# Public Ingress for hosting.haumdaucher.de (Recipes, Public Assets, Legacy Redirects)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
@@ -29,7 +51,7 @@ metadata:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
kubernetes.io/tls-acme: "true"
traefik.ingress.kubernetes.io/router.priority: "10"
traefik.ingress.kubernetes.io/router.middlewares: minio-hosting-legacy-redirect@kubernetescrd,minio-hosting-add-prefix@kubernetescrd
traefik.ingress.kubernetes.io/router.middlewares: minio-hosting-redirect-mila@kubernetescrd,minio-hosting-redirect-moritz@kubernetescrd,minio-hosting-redirect-family-root@kubernetescrd,minio-hosting-add-prefix@kubernetescrd
spec:
ingressClassName: traefik
rules:
@@ -48,8 +70,7 @@ spec:
- hosting.haumdaucher.de
secretName: hosting-haumdaucher-de-tls
---
# Protected Ingress for /family (Family Dashboards, Nutrition & Sleep Analytics)
# Protected by Authentik ForwardAuth
# Protected Ingress for /family (Redirects and Authentik Outpost)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
@@ -60,7 +81,7 @@ metadata:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
kubernetes.io/tls-acme: "true"
traefik.ingress.kubernetes.io/router.priority: "100"
traefik.ingress.kubernetes.io/router.middlewares: auth-authentik-forwardauth@kubernetescrd,minio-hosting-add-prefix@kubernetescrd
traefik.ingress.kubernetes.io/router.middlewares: minio-hosting-redirect-mila@kubernetescrd,minio-hosting-redirect-moritz@kubernetescrd,minio-hosting-redirect-family-root@kubernetescrd,auth-authentik-forwardauth@kubernetescrd,minio-hosting-add-prefix@kubernetescrd
spec:
ingressClassName: traefik
rules:
Binary file not shown.
+2 -19
View File
@@ -25,18 +25,7 @@ jobs:
delivery: "none"
command: "python3 /home/node/.openclaw/scripts/compact_sessions.py"
# 2. Family Dashboards & Web Analytics
- name: "dashboards-periodic-publish"
declarationKey: "dashboards-periodic-publish"
description: "Periodically regenerates and uploads Mila & Moritz family HTML dashboards to S3"
schedule:
kind: "every"
every: "10m"
session: "isolated"
delivery: "none"
command: "sh -lc 'export PATH=/home/node/.openclaw/bin:$PATH; cd /home/node/mop-skills/mop-skills/skills/datastore && uv run scripts/update_website.py --data-dir /home/node/.openclaw/wiki/main/data && uv run scripts/update_moritz_website.py --data-dir /home/node/.openclaw/wiki/main/data'"
# 3. Nightly Garmin Connect Mirror
# 2. Nightly Garmin Connect Mirror
- name: "garmin-nightly-mirror"
declarationKey: "garmin-nightly-mirror"
description: "Nächtlicher deterministischer Spiegel der Garmin Connect Daten (Aktivitäten, Daily Summary, Schlaf) in Git JSONL"
@@ -56,13 +45,7 @@ jobs:
Vorgehen:
1. Führe den Garmin-Sync-Befehl aus:
uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/garmin_sync.py --days 2 --data-dir /home/node/.openclaw/wiki/main/data
2. Aktualisiere die gerenderten Markdown-Ansichten:
PYTHONPATH=/home/node/mop-skills/mop-skills/skills/datastore/scripts uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/render_views.py --data-dir /home/node/.openclaw/wiki/main/data --wiki-dir /home/node/.openclaw/wiki/main
3. Aktualisiere das persönliche Health & Nutrition Dashboard (moritz.html):
uv run /home/node/mop-skills/mop-skills/skills/datastore/scripts/update_moritz_website.py --data-dir /home/node/.openclaw/wiki/main/data
4. Git Commit & Push im Knowledge Wiki:
cd /home/node/.openclaw/wiki/main && git add data/garmin/ entities/nutrition-log/ public/ && git diff --staged --quiet || (git commit -m "chore(garmin): automated nightly mirror [skip ci]" && git push origin main)
5. Finale Antwort:
2. Finale Antwort:
- Bei Erfolg: Gib exakt 'NO_REPLY' aus (intentional silence, kein Telegram-Spam in der Nacht).
- Nur bei Authentifizierungsfehlern: Informiere kurz via Telegram (Thema #Sport), dass ein Garmin-Re-Auth nötig ist, oder starte den Re-Auth-Flow.