37 Commits
Author SHA1 Message Date
chemavxandClaude Fable 5 e00c520a26 Decomisión de openclaw
- Elimina manifests openclaw/ y argocd/application-openclaw.yaml
- backup-system: fuera del backup diario, crown-jewels, expected y retain;
  fuera el mount hostPath del cronjob y la fila de RESTORE.md
- monitoring: eliminado panel openclaw del dashboard homelab-overview
- n8n: openclaw y polymarket fuera del health-check (espejo del workflow vivo)
- cluster-wide/namespaces.yaml: fuera openclaw + añadidos separadores '---'
  que faltaban (el archivo era un único doc YAML donde ganaba el último ns)

Cluster ya limpio: app ArgoCD, namespace, PVC/PV, RBAC cluster-scoped y
monitor de Uptime Kuma. Backup final: ~/decommissioned/openclaw-final-2026-07-18.tar.gz

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 14:57:46 +00:00
Gitea CI d011a4c50f ci: update roswell-corpus image to 09457a47 [skip ci] 2026-07-17 11:53:01 +00:00
Gitea CI 8a5ccbebdb ci: update roswell-corpus image to 93fe8de2 [skip ci] 2026-07-17 10:19:35 +00:00
chemavxandClaude Fable 5 09643943cb feat(portfolio): refresh de la landing — fix /ze/, fuera Open WebUI, nuevos servicios y proyectos
- fix: card ze apunta a /ze/ (el redirect de nginx a http:// caía en 404 de Traefik)
- fuera Open WebUI (chat.chemavx.xyz no existe) y card duplicada de polymarket en Services
- nuevos servicios: Uptime Kuma, Infisical, Trilium, Files, Umami, Ollama
- Projects: blogs The Exclusion Zone (EN) y Zona de Exclusión (ES) + ResearchOwl
- badges de infra bajo el header y subtítulos en las cards; Status en el footer

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 09:44:58 +00:00
Gitea CI 2abc398a10 ci: update roswell-corpus image to 5ac38bc0 [skip ci] 2026-07-17 09:38:49 +00:00
chemavxandClaude Fable 5 376eab68b6 decommission(F4): retirar polymarket-bot — Application y manifests
Fase final de la decomisión: namespace y Application borrados del cluster,
manifests fuera de git. Dump definitivo verificado y offsite en
/data/backups/backups/polymarket-decommission/ (espejo en mega:k3s-backups/).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 09:29:10 +00:00
Gitea CI 5c43a59884 ci: update roswell-corpus image to 3b55d68e [skip ci] 2026-07-17 08:59:22 +00:00
chemavxandClaude Fable 5 c99d0a978e chore(backup-system): consolidar config rclone en Infisical (mega+b2)
El bloque [b2] ya vive en RCLONE_CONF (Infisical /backup-system); los 3
CronJobs vuelven a rclone-conf-infisical y se elimina el secret
out-of-band rclone-conf-b2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 19:18:32 +00:00
chemavxandClaude Fable 5 c364d46ac6 feat(backup-system): segundo offsite Backblaze B2 (crown jewels)
- b2-crown-jewels.sh: sync diario 04:00 de las 2 copias mas recientes de
  cada servicio irremplazable (todo menos uptime-kuma) a
  b2:chemavx-k3s-backups/crown-jewels/ (~2GB, tier gratuito).
- reddit-intel.sh sube tambien a B2 (retencion 3d).
- verify.sh comprueba Mega (completo) + B2 (crown jewels).
- Config via Secret out-of-band rclone-conf-b2 (mega+b2, kubectl, NO git);
  consolidar en Infisical RCLONE_CONF cuando se anada [b2] por UI (la
  identity del operator es read-only por API).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 17:24:02 +00:00
chemavxandClaude Fable 5 39ea16fe91 fix(backup-system): excluir binarios reproducibles del tar de home (.warp, .local/share/claude, .local/lib)
1.5G -> ~500M por dia de churn en Mega; .local/bin y .local/state se conservan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 16:44:10 +00:00
chemavxandClaude Fable 5 e9eb8c4ae2 fix(backup-system): kubectl via ServiceAccount in-cluster, no kubeconfig del host
El k3s.yaml del host apunta a 127.0.0.1:6443 (inalcanzable desde el pod).
El script antiguo solo funcionaba porque su KUBECONFIG apuntaba a un
fichero inexistente y kubectl caia en fallback al SA. Se hace explicito:
unset KUBECONFIG + RBAC con apps/deployments,statefulsets get (para
kubectl exec deploy/...) y se retira el mount del kubeconfig.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 16:39:25 +00:00
chemavxandClaude Fable 5 ed3d886090 feat(backup-system): cobertura completa del homelab + sync diario a Mega
- backup.sh: fix openclaw (tar-eaba un hostPath stub vacio, 104 bytes/dia
  desde hace meses); ahora resuelve el PVC real via local-path.
- Nuevos servicios: ghost-en/zona-exclusion (VACUUM INTO consistente via
  node del pod + tar del content), gitea (sqlite .backup + repos + conf,
  sin packages 6.4G reconstruibles), researchowl (sqlite backup API via
  python3), roswell-pg, umami-pg, infisical-pg, trilium, vaultwarden,
  uptime-kuma (sqlite3 .backup), filebrowser-db, home-master (sin ~/.ssh
  ni caches), k3s token + /etc/rancher/k3s (cadena de restauracion).
- Validacion por artefacto: gzip -t + tamano minimo; el job sale 1 si
  falta algo -> alerta Grafana->Telegram existente.
- rclone-mega: de semanal a diario (03:30); --exclude reddit-intel/.
- Nuevo CronJob reddit-intel-backup en n97 (unico dato del worker fuera
  de k3s; su cron local escribia en el mismo disco).
- verify.sh: lista completa (20 servicios + reddit-intel), diario,
  ventana de frescura 3 dias (BusyBox-safe).
- backoffLimit 1 (retry recrearia artefactos y romperia la retencion).
- RESTORE.md: runbook con la cadena token->state.db->infisical-secrets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 16:12:27 +00:00
Gitea CI 4c0b2d4fa4 ci: update roswell-corpus image to f4c04d72 [skip ci] 2026-07-13 17:39:49 +00:00
Gitea CI a85c82a903 ci: update roswell-corpus image to c48a5f73 [skip ci] 2026-07-13 17:39:32 +00:00
chemavxandClaude Fable 5 005b13bd91 feat(roswell): corte a secretos Infisical — refs *-infisical en deployment, postgres y backup
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 17:37:14 +00:00
chemavxandClaude Fable 5 1b9a7bdddc fix(roswell): un InfisicalStaticSecret por target — el operator ignora targets extra
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 17:31:04 +00:00
Gitea CI 871af5df11 ci: update roswell-corpus image to 9efbb53b [skip ci] 2026-07-13 17:21:30 +00:00
chemavxandClaude Fable 5 d903fd4631 feat(roswell): postgres a ClusterIP y secretos Infisical preparados (alta manual pendiente en /roswell)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 17:20:25 +00:00
Gitea CI 43e607629f ci: update roswell-corpus image to 6d492bc9 [skip ci] 2026-07-13 17:12:03 +00:00
chemavxandClaude Fable 5 7e60b5e8d5 fix(roswell): pin a chemavx-k8 y límite 6Gi — Whisper large-v3 OOMKilled con 4Gi en el nodo pequeño
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 17:05:03 +00:00
Gitea CI 831d686b5a ci: update roswell-corpus image to 79a0a396 [skip ci] 2026-07-13 16:34:37 +00:00
chemavxandClaude Fable 5 6416c6c3af feat(roswell): despliegue GitOps del corpus — bot+scheduler, backup pg_dump diario, postgres adoptado
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 16:29:19 +00:00
Gitea CI ad3716bf80 ci: update researchowl image to dadc030d [skip ci] 2026-07-10 11:17:43 +00:00
Gitea CI 29b3fb3e5a ci: update researchowl image to 0e9a2e5b [skip ci] 2026-07-10 11:10:43 +00:00
Gitea CI 989064d332 ci: update researchowl image to 4ee9ad06 [skip ci] 2026-07-10 09:42:12 +00:00
chemavxandClaude Fable 5 91bf7d2909 fix(researchowl): memory limit 1Gi → 2Gi
El pod fue OOMKilled el 2026-07-10 durante un research: 20 fuentes
concurrentes con PDFs grandes y pdfplumber superaron 1Gi. El scraper
se endurece en paralelo (cap PDF 15MB, contenido 300k chars), pero el
margen extra evita que un pico legítimo mate la tarea en memoria.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 09:38:53 +00:00
chemavxandClaude Fable 5 cbfb1ba039 feat(n8n): inyectar TELEGRAM_BOT_TOKEN/CHAT_ID desde telegram-notify-infisical
Para que el workflow 'Health Check General' lea el token del bot de notificaciones
via process.env en vez de hardcodearlo (rotación 2026-07-09).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 20:54:21 +00:00
chemavxandClaude Fable 5 43c78246a8 security: eliminar token de Telegram en claro de ficheros versionados
- openclaw/golden/openclaw.json: botToken → placeholder (referencia Infisical)
- n8n/health-check-code-node.js: literal → process.env.TELEGRAM_BOT_TOKEN

Token del bot @chemavx_bot rotado 2026-07-09 (BotFather). El valor vivo se
gestiona en Infisical homelab/prod/telegram. NOTA: el token viejo sigue en el
HISTORIAL de git; la revocación en BotFather es lo que lo neutraliza.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 20:46:04 +00:00
chemavxandClaude Fable 5 395102a85e feat(n8n): auto-reload del deployment al cambiar n8n-secret-infisical (operator Infisical)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 15:16:33 +00:00
chemavxandClaude Fable 5 9dbcb0896e fix(n8n): N8N_BLOCK_ENV_ACCESS_IN_NODE=false — n8n bloquea $env por defecto y el autopost no podía leer GETXAPI_TOKEN
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 14:38:32 +00:00
chemavxandClaude Fable 5 f29256ca55 feat(n8n): inyectar GETXAPI_TOKEN desde n8n-secret-infisical (rotación token X autopost)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 14:30:20 +00:00
chemavxandClaude Fable 5 e1a25329d4 feat(monitoring): alerta Telegram para jobs fallidos de backup-system
Regla homelab-backup-job-failed (kube_job_status_failed{namespace="backup-system"} > 0)
en el grupo homelab-infra; cubre backup, rclone-mega-backup y backup-verify.
Aplicado con kubectl replace + rollout restart de Grafana (namespace manual).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 09:37:28 +00:00
chemavxandClaude Fable 5 d1d2fac287 feat(backup-system): migración completa a GitOps
- CronJob rclone-mega-backup entra en git (solo existía imperativo)
- Scripts backup.sh/verify.sh/rclone-mega.sh como ConfigMap backup-scripts
  (antes hostPath /data/backups/scripts — cambios ahora pasan por git+ArgoCD)
- rclone.conf vía InfisicalStaticSecret (homelab/prod//backup-system → RCLONE_CONF),
  sustituye al hostPath de ~/.config/rclone/rclone.conf
- Imágenes pinneadas: rclone 1.74.3, bitnami/kubectl por digest (v1.36.2)
- ClusterRole/CRB de backup-sa entran en git
- Application ArgoCD backup-system (auto-sync + prune + selfHeal)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 09:33:35 +00:00
chemavxandClaude Fable 5 ae0b5f9492 feat(reddit-intel): manifiestos preparados, INACTIVOS hasta promoción
App corre en docker compose en n97 (semana de calibración). El app-of-apps
no es recursivo: nada de este directorio se sincroniza hasta aplicar a mano
argocd-app.yaml. Checklist de activación en PROMOTION.md (imagen construida,
secrets en Infisical /reddit-intel, copia de la BD al PVC).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 17:14:06 +00:00
chemavx f059470fde decommission(F3): activar tarjeta Archived del bot en el portfolio
Merge de feat/portfolio-polymarket-archived con las cifras finales de
cierre (12 trades, +$247.78 realized, 239k evaluaciones, 81 días).
El widget deja de consultar la API (apagada) y pasa a tarjeta estática
con enlace al case study.
2026-07-06 12:20:48 +00:00
chemavx 8a89d0bd46 feat(portfolio): cifras finales del widget al cierre real (239k evaluaciones)
Snapshot del apagado 2026-07-06 con el bot ya detenido: 12 trades,
realized +$247.78, 239.462 evaluaciones, 81 días observados.
2026-07-06 12:16:23 +00:00
chemavxandClaude Fable 5 59bc269923 feat(portfolio): tarjeta Polymarket Bot pasa a Archived / case study
Sustituye el widget live (fetch a polymarket.chemavx.xyz/api/summary) por
una tarjeta estática con las cifras finales del paper trading y enlace al
case study en el repo. La tarjeta de servicio apunta al repo de Gitea en
lugar del dominio, que desaparecerá con el apagado.

NO MERGEAR HASTA F2/F3: mientras el bot siga vivo, main mantiene el
widget live. Aplicar esta rama cuando se apague el bot (Fase 3 del plan
de decomisión).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 07:40:09 +00:00
60 changed files with 1905 additions and 1488 deletions
-1
View File
@@ -27,7 +27,6 @@ Los secrets TLS los gestiona **cert-manager** automáticamente a partir del recu
| `authentik` | `authentik-secret` | Ver Vaultwarden → "authentik" | | `authentik` | `authentik-secret` | Ver Vaultwarden → "authentik" |
| `cloudflare-ddns` | `cloudflare-ddns-secret` | Ver Vaultwarden → "cloudflare-ddns" | | `cloudflare-ddns` | `cloudflare-ddns-secret` | Ver Vaultwarden → "cloudflare-ddns" |
| `vaultwarden` | `vaultwarden-secret` | Ver Vaultwarden → "vaultwarden" | | `vaultwarden` | `vaultwarden-secret` | Ver Vaultwarden → "vaultwarden" |
| `openclaw` | `openclaw-token` | Ver Vaultwarden → "openclaw" |
| `ghost-en` | `ghost-en-smtp` | `kubectl create secret generic ghost-en-smtp --from-literal=SMTP_USER='<user-gmail>' --from-literal=SMTP_PASS='<app-password>' -n ghost-en` | | `ghost-en` | `ghost-en-smtp` | `kubectl create secret generic ghost-en-smtp --from-literal=SMTP_USER='<user-gmail>' --from-literal=SMTP_PASS='<app-password>' -n ghost-en` |
| `argocd` | `argocd-secret` | Gestionado por ArgoCD bootstrap | | `argocd` | `argocd-secret` | Gestionado por ArgoCD bootstrap |
| `argocd` | `argocd-redis` | Gestionado por ArgoCD bootstrap | | `argocd` | `argocd-redis` | Gestionado por ArgoCD bootstrap |
-18
View File
@@ -1,18 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: polymarket-bot
namespace: argocd
spec:
destination:
namespace: polymarket-bot
server: https://kubernetes.default.svc
project: default
source:
path: polymarket-bot
repoURL: https://git.chemavx.xyz/chemavx/k8s-manifests
targetRevision: main
syncPolicy:
automated:
prune: true
selfHeal: true
+94
View File
@@ -0,0 +1,94 @@
# RESTORE.md — Runbook de restauración del homelab
> Última revisión: 2026-07-15. Si cambias `backup.sh`, actualiza este runbook.
## Dónde están las copias
- **Mega** (`mega:k3s-backups/`): espejo diario de `/data/backups/backups` del master
(retención 7 por servicio; 3 para `uptime-kuma` y `home-master`), más el subdir
`reddit-intel/` que sube el CronJob de n97.
- **Backblaze B2** (`b2:chemavx-k3s-backups/crown-jewels/` + `reddit-intel/`):
copia reducida "crown jewels" (últimas 2 de cada servicio irremplazable, todo
menos uptime-kuma). Segundo proveedor por si Mega falla.
- Acceso: credenciales rclone en Infisical (`/backup-system`, key `RCLONE_CONF`).
**Si el cluster está muerto, Infisical también** → usa el login de Mega/B2 de la
cuenta personal (Vaultwarden... que también está en el backup: guarda estas dos
contraseñas TAMBIÉN en un gestor externo al homelab, p. ej. el llavero del móvil).
## ⚠️ La cadena de dependencias (leer antes de restaurar nada)
```
token k3s ──> state.db (todos los Secrets k8s) ──> infisical-secrets (ENCRYPTION_KEY)
v
dump infisical-pg (secrets de apps)
v
resto de apps (n8n, ghost smtp, ddns, runner…)
```
1. `k3s-db_*.tar.gz` contiene `state.db` **+ `/var/lib/rancher/k3s/server/token` +
`/etc/rancher/k3s/`**. Sin ese token, un k3s nuevo NO puede leer el state.db
(los datos de bootstrap van cifrados con él).
2. El Secret `infisical-secrets` (ns `infisical`: `ENCRYPTION_KEY`, `AUTH_SECRET`)
está DENTRO de state.db. Sin él, el dump `infisical-pg` es indescifrable.
3. n8n es autosuficiente: su `encryptionKey` va dentro de `n8n_*.tar.gz`
(`.n8n/config`).
## Escenario A — Perder un PVC / una app (lo habitual)
Restaurar solo el artefacto de esa app (ver tabla). El cluster sigue vivo.
## Escenario B — Muere el disco del master (chemavx-k8)
1. Reinstalar Ubuntu + k3s server (misma versión mayor; ver skill k8s-infra).
2. Restaurar del `k3s-db_*.tar.gz`: `/etc/rancher/k3s/`, el fichero `token` en
`/var/lib/rancher/k3s/server/token` y `state.db*` en
`/var/lib/rancher/k3s/server/db/` (con k3s parado). Arrancar k3s.
3. Con el API server vivo: los manifests los trae ArgoCD desde Gitea → pero Gitea
está en el master: restaurar primero Gitea (tabla) para que ArgoCD sincronice.
4. Restaurar los datos de apps del master (tabla). Reinstalar Tailscale a mano
(está fuera de k8s).
## Escenario C — Muere el disco del worker (n97)
Reinstalar Ubuntu + k3s agent (token del master), dejar que los pods se
reprogramen y restaurar: ghost-en, zona-exclusion, researchowl, trilium,
umami-pg, roswell-pg (la BD corre en n97) y `/opt/reddit-intel` + Docker.
## Tabla de restauración por artefacto
| Artefacto | Cómo restaurar |
|---|---|
| `n8n_*.tar.gz` | Extraer en `/data/n8n` (hostPath master), reiniciar deploy. Incluye encryptionKey. |
| `homarr_*.tar.gz` | `db/db.sqlite` → PVC db; `redis/dump.rdb` → PVC redis. |
| `authentik-pg_*.sql.gz` | `gunzip -c \| kubectl exec -i postgresql-0 -n authentik -- psql -U authentik authentik` (BD vacía primero). |
| `grafana_*.tar.gz` | `grafana.db` → PVC de grafana, reiniciar pod. |
| `k3s-db_*.tar.gz` | Ver Escenario B. **No** restaurar sobre un cluster sano. |
| `gitea_*.tar.gz` | Extraer en el PVC de gitea; **renombrar `gitea/gitea-backup.db` → `gitea/gitea.db`**. El registry (packages) NO está: re-push de imágenes vía CI (re-run de Gitea Actions por app). |
| `ghost-*-db_*.db.gz` | `gunzip``content/data/ghost.db` (pod parado). |
| `ghost-*-content_*.tar.gz` | Extraer en `/var/lib/ghost/content` (incluye imágenes, temas, `data/redirects.yaml`; el `data/ghost.db` del tar es copia en caliente — usa el `-db_*.db.gz`, que es la consistente). |
| `researchowl_*.db.gz` | `gunzip``/data/researchowl.db` en el PVC (pod parado; borrar `-wal`/`-shm` viejos). |
| `roswell-pg_*.sql.gz` / `umami-pg_*.sql.gz` | Como authentik: `psql` contra el deploy/postgres de su namespace. |
| `infisical-pg_*.sql.gz` | **Primero** debe existir el Secret `infisical-secrets` original (viene en state.db; si no, exportarlo ANTES de perderlo). Luego psql contra `infisicalDB` en `postgresql-0`. |
| `trilium_*.tar.gz` | Extraer en `/home/node/trilium-data` (PVC, pod parado). |
| `vaultwarden_*.tar.gz` | Extraer en el PVC (pod parado). Incluye db, attachments y claves RSA. |
| `uptime-kuma_*.db.gz` | `gunzip``/app/data/kuma.db` (pod parado, borrar `-wal`/`-shm`). |
| `filebrowser-db_*.tar.gz` | Extraer en el PVC `filebrowser-db`. |
| `home-master_*.tar.gz` | Extraer en `/home/chemavx`. **No incluye `~/.ssh`** (regenerar claves) ni `~/bin` ni venvs/caches (reinstalar). |
| `reddit-intel/reddit-intel_*.tar.gz` | Extraer en `/opt/reddit-intel` de n97, `docker compose up -d`. |
## Qué NO está en el backup (reconstruible, asumido)
- Registry de imágenes de Gitea (re-build por CI), modelos Ollama (re-pull),
hf-cache de roswell, Prometheus TSDB (histórico de métricas se pierde),
registry-cache, SO de los nodos, Tailscale (re-join), certificados TLS
(cert-manager los re-emite), DNS (favonia lo reconstruye).
- `~/.ssh` del master: excluido a propósito — regenerar par ed25519 y
re-autorizar en n97/Gitea.
## Simulacro
Tras cualquier cambio gordo en `backup.sh`, repetir el simulacro: bajar de Mega
el `ghost-en-db` más reciente, `PRAGMA integrity_check` + `SELECT count(*) FROM posts`,
y extraer un `home-master` en /tmp comprobando que `~/.claude/` está dentro.
+23
View File
@@ -0,0 +1,23 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: backup-system
namespace: argocd
annotations:
notifications.argoproj.io/subscribe.on-sync-failed.telegram: "5138407666"
notifications.argoproj.io/subscribe.on-health-degraded.telegram: "5138407666"
spec:
project: default
source:
repoURL: https://git.chemavx.xyz/chemavx/k8s-manifests
targetRevision: HEAD
path: backup-system
destination:
server: https://kubernetes.default.svc
namespace: backup-system
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
+23
View File
@@ -0,0 +1,23 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: backup-role
rules:
- apiGroups: [""]
resources:
- pods
- pods/exec
- persistentvolumeclaims
- persistentvolumes
verbs:
- get
- list
- create
# kubectl exec deploy/<nombre> resuelve el Deployment antes de elegir pod
- apiGroups: ["apps"]
resources:
- deployments
- statefulsets
verbs:
- get
- list
@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: backup-role-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: backup-role
subjects:
- kind: ServiceAccount
name: backup-sa
namespace: backup-system
+380
View File
@@ -0,0 +1,380 @@
apiVersion: v1
data:
backup.sh: |
#!/bin/bash
# Backup diario del homelab. Imagen: bitnami/kubectl (bash GNU, corre en chemavx-k8).
# Regla: cada artefacto se valida (gzip -t + tamaño mínimo) — un tar vacío
# (bug histórico: tar de 104 bytes de una app ya decomisada) cuenta como FALLO y el job sale 1
# (→ alerta Grafana homelab-backup-job-failed → Telegram).
set -u
# kubectl usa el ServiceAccount in-cluster (backup-sa + ClusterRole backup-role).
# NO usar el k3s.yaml del host: su server es https://127.0.0.1:6443, inalcanzable
# desde la red del pod (el script antiguo "funcionaba" porque apuntaba KUBECONFIG
# a un fichero inexistente y kubectl caía en fallback al ServiceAccount).
unset KUBECONFIG
BACKUP_DIR="/data/backups/backups"
DATE=$(date +%Y-%m-%d_%H-%M-%S)
STORAGE=/var/lib/rancher/k3s/storage
FAIL=0
mkdir -p "$BACKUP_DIR"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
fail() { log "ERROR: $1 FAILED"; FAIL=$((FAIL + 1)); }
# Valida artefacto: gzip íntegro y tamaño >= mínimo esperado
check() { # $1=fichero $2=bytes_minimos $3=servicio
if gzip -t "$1" 2>/dev/null && [ "$(stat -c%s "$1" 2>/dev/null || echo 0)" -ge "$2" ]; then
log "$3 OK ($(du -h "$1" | cut -f1))"
else
rm -f "$1"
fail "$3 (artefacto corrupto o menor de $2 bytes)"
fi
}
# Resuelve la ruta local-path de un PVC en el host
pvpath() { # $1=ns $2=pvc
local pv
pv=$(kubectl get pvc -n "$1" "$2" -o jsonpath='{.spec.volumeName}' 2>/dev/null) || return 1
echo "$STORAGE/${pv}_$1_$2"
}
# Copia consistente de la ghost.db (VACUUM INTO vía node del pod) + tar del content
backup_ghost() { # $1=ns $2=deploy $3=prefijo
local f="$BACKUP_DIR/${3}-db_${DATE}.db.gz"
kubectl exec -i -n "$1" "deploy/$2" -- sh -c \
'rm -f /tmp/ghost-backup.db && NODE_PATH=/var/lib/ghost/current/node_modules node -' \
< /scripts/ghost-db-backup.js >/dev/null 2>&1 \
&& kubectl exec -n "$1" "deploy/$2" -- cat /tmp/ghost-backup.db 2>/dev/null | gzip > "$f"
kubectl exec -n "$1" "deploy/$2" -- rm -f /tmp/ghost-backup.db 2>/dev/null || true
check "$f" 200000 "${3}-db"
local c="$BACKUP_DIR/${3}-content_${DATE}.tar.gz"
kubectl exec -n "$1" "deploy/$2" -- tar czf - -C /var/lib/ghost/content . > "$c" 2>/dev/null
check "$c" 5000000 "${3}-content"
}
# === n8n (hostPath /data/n8n — incluye .n8n/config con la encryptionKey) ===
log "Backing up n8n..."
tar -czf "$BACKUP_DIR/n8n_${DATE}.tar.gz" -C /data/n8n . 2>/dev/null
check "$BACKUP_DIR/n8n_${DATE}.tar.gz" 10000000 n8n
# === homarr ===
log "Backing up homarr..."
HOMARR_DB=$(pvpath homarr homarr-db-pvc)
HOMARR_REDIS=$(pvpath homarr homarr-redis-pvc)
mkdir -p /tmp/backup-homarr/db /tmp/backup-homarr/redis
cp "$HOMARR_DB/db.sqlite" /tmp/backup-homarr/db/ 2>/dev/null || true
cp "$HOMARR_REDIS/dump.rdb" /tmp/backup-homarr/redis/ 2>/dev/null || true
tar -czf "$BACKUP_DIR/homarr_${DATE}.tar.gz" -C /tmp/backup-homarr . 2>/dev/null
check "$BACKUP_DIR/homarr_${DATE}.tar.gz" 10000 homarr
rm -rf /tmp/backup-homarr
# === authentik PostgreSQL ===
log "Backing up authentik postgres..."
kubectl exec -n authentik postgresql-0 -- pg_dump -U authentik authentik 2>/dev/null | \
gzip > "$BACKUP_DIR/authentik-pg_${DATE}.sql.gz"
check "$BACKUP_DIR/authentik-pg_${DATE}.sql.gz" 10000 authentik-pg
# === grafana ===
log "Backing up grafana..."
GRAFANA=$(pvpath monitoring kube-prometheus-stack-grafana)
mkdir -p /tmp/backup-grafana
cp "$GRAFANA/grafana.db" /tmp/backup-grafana/ 2>/dev/null || true
tar -czf "$BACKUP_DIR/grafana_${DATE}.tar.gz" -C /tmp/backup-grafana . 2>/dev/null
check "$BACKUP_DIR/grafana_${DATE}.tar.gz" 100000 grafana
rm -rf /tmp/backup-grafana
# === k3s: state.db + token del server + /etc/rancher/k3s ===
# Sin el token, state.db NO arranca en un k3s nuevo (cadena de restauración).
log "Backing up k3s (state.db + token + /etc/rancher/k3s)..."
tar -czf "$BACKUP_DIR/k3s-db_${DATE}.tar.gz" \
/data/k3s-db/state.db* /host/k3s-token /host/etc-rancher-k3s 2>/dev/null
check "$BACKUP_DIR/k3s-db_${DATE}.tar.gz" 1000000 k3s-db
# === Gitea: sqlite .backup consistente + repos + conf (SIN gitea/packages, 6.4G de registry reconstruible por CI) ===
log "Backing up gitea..."
kubectl exec -n gitea gitea-0 -- sh -c \
'rm -f /data/gitea/gitea-backup.db && sqlite3 /data/gitea/gitea.db ".backup /data/gitea/gitea-backup.db"' 2>/dev/null \
&& kubectl exec -n gitea gitea-0 -- tar czf - -C /data \
git ssh gitea/conf gitea/jwt gitea/avatars gitea/repo-avatars gitea/gitea-backup.db \
> "$BACKUP_DIR/gitea_${DATE}.tar.gz" 2>/dev/null
kubectl exec -n gitea gitea-0 -- rm -f /data/gitea/gitea-backup.db 2>/dev/null || true
check "$BACKUP_DIR/gitea_${DATE}.tar.gz" 5000000 gitea
# === Ghost EN + ES (db consistente + content con imágenes/temas/redirects) ===
log "Backing up ghost-en..."
backup_ghost ghost-en ghost-en ghost-en
log "Backing up zona-exclusion..."
backup_ghost zona-exclusion zona-exclusion zona-exclusion
# === ResearchOwl (sqlite .backup vía python3 del pod) ===
log "Backing up researchowl..."
kubectl exec -i -n researchowl deploy/researchowl -- sh -c \
'rm -f /tmp/ro-backup.db && python3 -' < /scripts/sqlite-stdin-backup.py >/dev/null 2>&1 \
&& kubectl exec -n researchowl deploy/researchowl -- cat /tmp/ro-backup.db 2>/dev/null | \
gzip > "$BACKUP_DIR/researchowl_${DATE}.db.gz"
kubectl exec -n researchowl deploy/researchowl -- rm -f /tmp/ro-backup.db 2>/dev/null || true
check "$BACKUP_DIR/researchowl_${DATE}.db.gz" 5000000 researchowl
# === Roswell (corpus) PostgreSQL ===
log "Backing up roswell postgres..."
kubectl exec -n roswell deploy/postgres -- sh -c \
'PGPASSWORD="${POSTGRES_PASSWORD:-}" pg_dump -U "$POSTGRES_USER" "${POSTGRES_DB:-$POSTGRES_USER}"' 2>/dev/null | \
gzip > "$BACKUP_DIR/roswell-pg_${DATE}.sql.gz"
check "$BACKUP_DIR/roswell-pg_${DATE}.sql.gz" 5000 roswell-pg
# === Umami PostgreSQL ===
log "Backing up umami postgres..."
kubectl exec -n umami deploy/postgres -- sh -c \
'PGPASSWORD="${POSTGRES_PASSWORD:-}" pg_dump -U "$POSTGRES_USER" "${POSTGRES_DB:-$POSTGRES_USER}"' 2>/dev/null | \
gzip > "$BACKUP_DIR/umami-pg_${DATE}.sql.gz"
check "$BACKUP_DIR/umami-pg_${DATE}.sql.gz" 5000 umami-pg
# === Infisical PostgreSQL (RESTAURAR REQUIERE infisical-secrets — está dentro de k3s-db/state.db, ver RESTORE.md) ===
log "Backing up infisical postgres..."
kubectl exec -n infisical postgresql-0 -- sh -c \
'PGPASSWORD="$POSTGRES_PASSWORD" pg_dump -U "$POSTGRES_USER" infisicalDB' 2>/dev/null | \
gzip > "$BACKUP_DIR/infisical-pg_${DATE}.sql.gz"
check "$BACKUP_DIR/infisical-pg_${DATE}.sql.gz" 10000 infisical-pg
# === Trilium ===
log "Backing up trilium..."
kubectl exec -n trilium deploy/trilium -- tar czf - -C /home/node/trilium-data . \
> "$BACKUP_DIR/trilium_${DATE}.tar.gz" 2>/dev/null
check "$BACKUP_DIR/trilium_${DATE}.tar.gz" 500000 trilium
# === Vaultwarden (db + attachments + claves RSA; copia en frío ~02:00) ===
log "Backing up vaultwarden..."
VW=$(pvpath vaultwarden vaultwarden-pvc)
tar -czf "$BACKUP_DIR/vaultwarden_${DATE}.tar.gz" -C "$VW" . 2>/dev/null
check "$BACKUP_DIR/vaultwarden_${DATE}.tar.gz" 100000 vaultwarden
# === Uptime-Kuma (sqlite .backup vía CLI del pod; retención corta, es el artefacto gordo) ===
log "Backing up uptime-kuma..."
kubectl exec -n monitoring deploy/uptime-kuma -- sh -c \
'rm -f /tmp/kuma-backup.db && sqlite3 /app/data/kuma.db ".backup /tmp/kuma-backup.db"' 2>/dev/null \
&& kubectl exec -n monitoring deploy/uptime-kuma -- cat /tmp/kuma-backup.db 2>/dev/null | \
gzip > "$BACKUP_DIR/uptime-kuma_${DATE}.db.gz"
kubectl exec -n monitoring deploy/uptime-kuma -- rm -f /tmp/kuma-backup.db 2>/dev/null || true
check "$BACKUP_DIR/uptime-kuma_${DATE}.db.gz" 1000000 uptime-kuma
# === Filebrowser (solo su BD; los ficheros servidos van en home-master/imagefinder) ===
log "Backing up filebrowser db..."
FB=$(pvpath filebrowser filebrowser-db)
tar -czf "$BACKUP_DIR/filebrowser-db_${DATE}.tar.gz" -C "$FB" . 2>/dev/null
check "$BACKUP_DIR/filebrowser-db_${DATE}.tar.gz" 1000 filebrowser-db
# === Home del master (~/.claude memoria+skills, imagefinder, proyectos sueltos, RUNBOOK...) ===
# Excluye ~/.ssh (claves privadas, decisión explícita), caches y binarios reproducibles.
log "Backing up home-master..."
tar -czf "$BACKUP_DIR/home-master_${DATE}.tar.gz" -C /host/home-chemavx \
--exclude='./.ssh' --exclude='./.cache' --exclude='./.venvs' --exclude='./.npm' \
--exclude='./.nvm' --exclude='./bin' --exclude='./.docker' --exclude='./snap' \
--exclude='*/node_modules' --exclude='./.local/share/pnpm' \
--exclude='./.warp' --exclude='./.local/share/claude' --exclude='./.local/lib' \
. 2>/dev/null
check "$BACKUP_DIR/home-master_${DATE}.tar.gz" 50000000 home-master
# === Retención (7 por defecto; 3 para los artefactos gordos) ===
log "Cleaning old backups..."
retain() { # $1=prefijo $2=copias
find "$BACKUP_DIR" -maxdepth 1 \
\( -name "${1}_*.tar.gz" -o -name "${1}_*.sql.gz" -o -name "${1}_*.db.gz" \) | \
sort | head -n -"$2" | xargs -r rm -f
}
for p in n8n homarr authentik-pg grafana k3s-db gitea \
ghost-en-db ghost-en-content zona-exclusion-db zona-exclusion-content \
researchowl roswell-pg umami-pg infisical-pg trilium vaultwarden filebrowser-db; do
retain "$p" 7
done
retain uptime-kuma 3
retain home-master 3
if [ "$FAIL" -gt 0 ]; then
log "Backup terminado con $FAIL fallo(s) — el job sale con error para disparar la alerta."
exit 1
fi
log "Backup complete!"
ghost-db-backup.js: |
// Copia consistente de la ghost.db usando el driver sqlite3 del propio Ghost.
// Se ejecuta DENTRO del pod vía: kubectl exec -i ... node - (script por stdin)
const sqlite3 = require('sqlite3');
const src = '/var/lib/ghost/content/data/ghost.db';
const dst = '/tmp/ghost-backup.db';
const db = new sqlite3.Database(src, sqlite3.OPEN_READONLY, (err) => {
if (err) { console.error(err.message); process.exit(1); }
});
db.run("VACUUM INTO '" + dst + "'", (err) => {
if (err) { console.error(err.message); process.exit(1); }
db.close(() => console.log('ghost db backup ok'));
});
sqlite-stdin-backup.py: |
# Copia consistente de researchowl.db (API backup de sqlite3, stdlib).
# Se ejecuta DENTRO del pod vía: kubectl exec -i ... python3 -
import sqlite3
src = sqlite3.connect('file:/data/researchowl.db?mode=ro', uri=True)
dst = sqlite3.connect('/tmp/ro-backup.db')
with dst:
src.backup(dst)
dst.close()
src.close()
print('researchowl db backup ok')
rclone-mega.sh: |
#!/bin/bash
set -e
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
log "Iniciando sync a Mega..."
log "Origen: /data/backups/backups"
log "Destino: mega:k3s-backups/"
# --exclude reddit-intel: ese subdir remoto lo escribe el CronJob de n97
# directamente; un sync sin exclude lo borraría (sync = espejo exacto).
rclone sync /data/backups/backups mega:k3s-backups/ \
--exclude "/reddit-intel/**" \
--config /rclone/rclone.conf \
--log-level INFO \
--stats 30s
log "Sync completado."
log "Archivos en Mega:"
rclone ls mega:k3s-backups/ --config /rclone/rclone.conf
b2-crown-jewels.sh: |
#!/bin/sh
# Segunda copia offsite (Backblaze B2): "crown jewels" — las 2 copias más
# recientes de cada servicio irremplazable (~2GB, cabe en los 10GB gratis).
# Todo menos uptime-kuma (622M de historial de heartbeats, prescindible).
# Imagen BusyBox — sin GNU-ismos. Config: RCLONE_CONF de Infisical (mega+b2).
set -e
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
BACKUP_DIR=/data/backups/backups
CROWN="n8n homarr authentik-pg grafana k3s-db gitea ghost-en-db ghost-en-content zona-exclusion-db zona-exclusion-content researchowl roswell-pg umami-pg infisical-pg trilium vaultwarden filebrowser-db home-master"
KEEP=/tmp/crown-jewels
rm -rf "$KEEP"; mkdir -p "$KEEP"
for p in $CROWN; do
for f in $(ls -1 "$BACKUP_DIR/${p}"_* 2>/dev/null | sort | tail -2); do
ln -s "$f" "$KEEP/$(basename "$f")"
done
done
N=$(ls -1 "$KEEP" | wc -l)
log "Sync de $N artefactos crown-jewels a B2..."
# -L: resuelve los symlinks; sync = espejo → la retención remota (2/servicio) es automática
rclone sync "$KEEP" b2:chemavx-k3s-backups/crown-jewels/ -L \
--config /rclone/rclone.conf --log-level INFO --stats 60s
rm -rf "$KEEP"
log "B2 crown-jewels completado ($N artefactos)."
reddit-intel.sh: |
#!/bin/sh
# Corre en chemavx-n97 (imagen rclone, BusyBox — sin GNU-ismos).
# Sube /opt/reddit-intel a Mega directamente: es el único dato del host n97
# y su cron local escribe en el MISMO disco (inútil contra fallo de disco).
set -e
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
DATE=$(date +%Y-%m-%d_%H-%M-%S)
F="/tmp/reddit-intel_${DATE}.tar.gz"
log "Empaquetando /opt/reddit-intel..."
tar -czf "$F" -C /opt/reddit-intel .
gzip -t "$F"
log "Subiendo a mega:k3s-backups/reddit-intel/..."
rclone copy "$F" mega:k3s-backups/reddit-intel/ --config /rclone/rclone.conf --log-level INFO
log "Podando copias remotas de más de 8 días..."
rclone delete mega:k3s-backups/reddit-intel/ --min-age 8d --config /rclone/rclone.conf
log "Subiendo a B2 (crown jewels, retención 3 días)..."
rclone copy "$F" b2:chemavx-k3s-backups/reddit-intel/ --config /rclone/rclone.conf --log-level INFO
rclone delete b2:chemavx-k3s-backups/reddit-intel/ --min-age 3d --config /rclone/rclone.conf
rm -f "$F"
log "reddit-intel OK"
verify.sh: |
#!/bin/sh
# Imagen BusyBox (rclone) — evitar GNU-ismos (date -d 'yesterday' NO funciona).
set -e
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
EXPECTED_SERVICES="n8n homarr authentik-pg grafana k3s-db gitea ghost-en-db ghost-en-content zona-exclusion-db zona-exclusion-content researchowl roswell-pg umami-pg infisical-pg trilium vaultwarden uptime-kuma filebrowser-db home-master"
ERRORS=0
# Ventana de frescura: backup y sync son diarios → un backup es "reciente"
# si su fecha está en los últimos 3 días (margen para jobs nocturnos largos).
NOW=$(date +%s)
ACCEPTED_DATES=""
for i in $(seq 0 2); do
ACCEPTED_DATES="$ACCEPTED_DATES $(date -d @$(( NOW - i * 86400 )) +%Y-%m-%d)"
done
log "=== Verificación de backups en Mega ==="
FILES=$(rclone ls mega:k3s-backups/ --config /rclone/rclone.conf 2>&1)
log "Fechas aceptadas (ventana de 3 días):$ACCEPTED_DATES"
for svc in $EXPECTED_SERVICES; do
FOUND=0
for d in $ACCEPTED_DATES; do
if echo "$FILES" | grep -q "${svc}_${d}"; then
FOUND=1
break
fi
done
if [ "$FOUND" -eq 1 ]; then
log "[OK] $svc"
else
log "[WARN] $svc — sin backup en los últimos 3 días"
ERRORS=$((ERRORS + 1))
fi
done
# reddit-intel vive en su subdir (lo sube el CronJob de n97)
FOUND=0
for d in $ACCEPTED_DATES; do
if echo "$FILES" | grep -q "reddit-intel/reddit-intel_${d}"; then
FOUND=1
break
fi
done
if [ "$FOUND" -eq 1 ]; then
log "[OK] reddit-intel"
else
log "[WARN] reddit-intel — sin backup en los últimos 3 días"
ERRORS=$((ERRORS + 1))
fi
log "---"
TOTAL=$(echo "$FILES" | grep -c "tar.gz\|sql.gz\|db.gz" || true)
log "Total archivos en Mega: $TOTAL"
# === Segundo offsite: B2 crown-jewels (todo menos uptime-kuma) ===
log "=== Verificación de crown-jewels en B2 ==="
CROWN="n8n homarr authentik-pg grafana k3s-db gitea ghost-en-db ghost-en-content zona-exclusion-db zona-exclusion-content researchowl roswell-pg umami-pg infisical-pg trilium vaultwarden filebrowser-db home-master"
B2FILES=$(rclone ls b2:chemavx-k3s-backups/ --config /rclone/rclone.conf 2>&1)
for svc in $CROWN; do
FOUND=0
for d in $ACCEPTED_DATES; do
if echo "$B2FILES" | grep -q "${svc}_${d}"; then
FOUND=1
break
fi
done
if [ "$FOUND" -eq 1 ]; then
log "[OK] b2/$svc"
else
log "[WARN] b2/$svc — sin backup en los últimos 3 días"
ERRORS=$((ERRORS + 1))
fi
done
if [ "$ERRORS" -gt 0 ]; then
log "RESULTADO: $ERRORS servicio(s) sin backup reciente. Revisar."
exit 1
fi
log "RESULTADO: Todos los backups verificados correctamente (Mega + B2)."
kind: ConfigMap
metadata:
name: backup-scripts
namespace: backup-system
+14 -9
View File
@@ -4,7 +4,8 @@ metadata:
name: backup-verify name: backup-verify
namespace: backup-system namespace: backup-system
spec: spec:
schedule: "0 4 * * 1" # Diario desde 2026-07-15 (el sync a Mega ya es diario; ventana de frescura 3 días)
schedule: "0 5 * * *"
concurrencyPolicy: Forbid concurrencyPolicy: Forbid
failedJobsHistoryLimit: 3 failedJobsHistoryLimit: 3
successfulJobsHistoryLimit: 3 successfulJobsHistoryLimit: 3
@@ -19,7 +20,8 @@ spec:
runAsUser: 0 runAsUser: 0
containers: containers:
- name: verify - name: verify
image: rclone/rclone:latest # OJO: imagen BusyBox — verify.sh debe evitar GNU-ismos (date -d 'yesterday')
image: docker.io/rclone/rclone:1.74.3
command: ["/bin/sh", "/scripts/verify.sh"] command: ["/bin/sh", "/scripts/verify.sh"]
resources: resources:
limits: limits:
@@ -29,16 +31,19 @@ spec:
cpu: 50m cpu: 50m
memory: 64Mi memory: 64Mi
volumeMounts: volumeMounts:
- mountPath: /rclone/rclone.conf - mountPath: /rclone
name: rclone-conf name: rclone-conf
- mountPath: /scripts - mountPath: /scripts
name: scripts name: scripts
volumes: volumes:
# RCLONE_CONF (Infisical /backup-system) contiene mega+b2 desde 2026-07-15
- name: rclone-conf - name: rclone-conf
hostPath: secret:
path: /home/chemavx/.config/rclone/rclone.conf secretName: rclone-conf-infisical
type: File items:
- key: RCLONE_CONF
path: rclone.conf
- name: scripts - name: scripts
hostPath: configMap:
path: /data/backups/scripts name: backup-scripts
type: Directory defaultMode: 0755
+36 -24
View File
@@ -10,9 +10,13 @@ spec:
failedJobsHistoryLimit: 1 failedJobsHistoryLimit: 1
jobTemplate: jobTemplate:
spec: spec:
# Sin reintentos automáticos: un reintento re-crea TODOS los artefactos con
# otro timestamp y la retención por número podría expulsar copias buenas.
backoffLimit: 1
activeDeadlineSeconds: 3600
template: template:
spec: spec:
restartPolicy: OnFailure restartPolicy: Never
serviceAccountName: backup-sa serviceAccountName: backup-sa
securityContext: securityContext:
runAsUser: 0 runAsUser: 0
@@ -20,34 +24,38 @@ spec:
kubernetes.io/hostname: chemavx-k8 kubernetes.io/hostname: chemavx-k8
containers: containers:
- name: backup - name: backup
image: bitnami/kubectl:latest # bitnami/kubectl v1.36.2 — pinneado por digest: los tags de bitnami
# en Docker Hub ya no son fiables (migración Broadcom/bitnamilegacy)
image: docker.io/bitnami/kubectl@sha256:558420daf32bbc382e3e9af4537f4073085b336ddd47399a3b70e70087115978
command: ["/bin/bash", "/scripts/backup.sh"] command: ["/bin/bash", "/scripts/backup.sh"]
env:
- name: KUBECONFIG
value: /kubeconfig/k3s.yaml
resources: resources:
requests: requests:
cpu: 100m cpu: 200m
memory: 128Mi memory: 256Mi
limits: limits:
cpu: 500m cpu: "1"
memory: 512Mi memory: 1Gi
volumeMounts: volumeMounts:
- name: backup-dir - name: backup-dir
mountPath: /data/backups/backups mountPath: /data/backups/backups
- name: n8n-data - name: n8n-data
mountPath: /data/n8n mountPath: /data/n8n
- name: openclaw-data
mountPath: /data/openclaw
- name: scripts - name: scripts
mountPath: /scripts mountPath: /scripts
- name: k3s-storage - name: k3s-storage
mountPath: /var/lib/rancher/k3s/storage mountPath: /var/lib/rancher/k3s/storage
- name: kubeconfig
mountPath: /kubeconfig/k3s.yaml
- name: k3s-db - name: k3s-db
mountPath: /data/k3s-db mountPath: /data/k3s-db
readOnly: true readOnly: true
- name: k3s-token
mountPath: /host/k3s-token
readOnly: true
- name: etc-rancher-k3s
mountPath: /host/etc-rancher-k3s
readOnly: true
- name: home-chemavx
mountPath: /host/home-chemavx
readOnly: true
volumes: volumes:
- name: backup-dir - name: backup-dir
hostPath: hostPath:
@@ -57,23 +65,27 @@ spec:
hostPath: hostPath:
path: /data/n8n path: /data/n8n
type: DirectoryOrCreate type: DirectoryOrCreate
- name: openclaw-data
hostPath:
path: /data/openclaw
type: DirectoryOrCreate
- name: scripts - name: scripts
hostPath: configMap:
path: /data/backups/scripts name: backup-scripts
type: Directory defaultMode: 0755
- name: k3s-storage - name: k3s-storage
hostPath: hostPath:
path: /var/lib/rancher/k3s/storage path: /var/lib/rancher/k3s/storage
type: Directory type: Directory
- name: kubeconfig
hostPath:
path: /etc/rancher/k3s/k3s.yaml
type: File
- name: k3s-db - name: k3s-db
hostPath: hostPath:
path: /var/lib/rancher/k3s/server/db path: /var/lib/rancher/k3s/server/db
type: Directory type: Directory
- name: k3s-token
hostPath:
path: /var/lib/rancher/k3s/server/token
type: File
- name: etc-rancher-k3s
hostPath:
path: /etc/rancher/k3s
type: Directory
- name: home-chemavx
hostPath:
path: /home/chemavx
type: Directory
+58
View File
@@ -0,0 +1,58 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: rclone-b2-backup
namespace: backup-system
spec:
# Segunda copia offsite (crown jewels → Backblaze B2), tras el sync a Mega
schedule: "0 4 * * *"
concurrencyPolicy: Forbid
failedJobsHistoryLimit: 2
successfulJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 1
activeDeadlineSeconds: 3600
template:
spec:
nodeSelector:
kubernetes.io/hostname: chemavx-k8
restartPolicy: Never
securityContext:
runAsUser: 0
containers:
- name: rclone
# OJO: imagen BusyBox — el script debe evitar GNU-ismos
image: docker.io/rclone/rclone:1.74.3
command: ["/bin/sh", "/scripts/b2-crown-jewels.sh"]
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
volumeMounts:
- mountPath: /data/backups/backups
name: backup-dir
readOnly: true
- mountPath: /rclone
name: rclone-conf
- mountPath: /scripts
name: scripts
volumes:
- name: backup-dir
hostPath:
path: /data/backups/backups
type: Directory
# RCLONE_CONF (Infisical /backup-system) contiene mega+b2 desde 2026-07-15
- name: rclone-conf
secret:
secretName: rclone-conf-infisical
items:
- key: RCLONE_CONF
path: rclone.conf
- name: scripts
configMap:
name: backup-scripts
defaultMode: 0755
+54
View File
@@ -0,0 +1,54 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: rclone-mega-backup
namespace: backup-system
spec:
# Diario desde 2026-07-15 (antes semanal: un fallo de disco entre domingos
# perdía hasta 6 días). Corre tras el backup de las 02:00.
schedule: "30 3 * * *"
concurrencyPolicy: Forbid
failedJobsHistoryLimit: 2
successfulJobsHistoryLimit: 3
jobTemplate:
spec:
template:
spec:
nodeSelector:
kubernetes.io/hostname: chemavx-k8
restartPolicy: OnFailure
securityContext:
runAsUser: 0
containers:
- name: rclone
image: docker.io/rclone/rclone:1.74.3
command: ["/bin/sh", "/scripts/rclone-mega.sh"]
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
volumeMounts:
- mountPath: /data/backups/backups
name: backup-dir
- mountPath: /rclone
name: rclone-conf
- mountPath: /scripts
name: scripts
volumes:
- name: backup-dir
hostPath:
path: /data/backups/backups
type: DirectoryOrCreate
- name: rclone-conf
secret:
secretName: rclone-conf-infisical
items:
- key: RCLONE_CONF
path: rclone.conf
- name: scripts
configMap:
name: backup-scripts
defaultMode: 0755
+59
View File
@@ -0,0 +1,59 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: reddit-intel-backup
namespace: backup-system
spec:
# Único CronJob de backup que corre en n97: /opt/reddit-intel vive fuera de k3s
# (Docker en el host) y su cron local escribe en el mismo disco.
schedule: "15 3 * * *"
concurrencyPolicy: Forbid
failedJobsHistoryLimit: 2
successfulJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 1
activeDeadlineSeconds: 1800
template:
spec:
nodeSelector:
kubernetes.io/hostname: chemavx-n97
restartPolicy: Never
securityContext:
runAsUser: 0
containers:
- name: rclone
# OJO: imagen BusyBox — el script debe evitar GNU-ismos
image: docker.io/rclone/rclone:1.74.3
command: ["/bin/sh", "/scripts/reddit-intel.sh"]
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 256Mi
volumeMounts:
- mountPath: /opt/reddit-intel
name: reddit-intel
readOnly: true
- mountPath: /rclone
name: rclone-conf
- mountPath: /scripts
name: scripts
volumes:
- name: reddit-intel
hostPath:
path: /opt/reddit-intel
type: Directory
# RCLONE_CONF (Infisical /backup-system) contiene mega+b2 desde 2026-07-15
- name: rclone-conf
secret:
secretName: rclone-conf-infisical
items:
- key: RCLONE_CONF
path: rclone.conf
- name: scripts
configMap:
name: backup-scripts
defaultMode: 0755
+24
View File
@@ -0,0 +1,24 @@
apiVersion: secrets.infisical.com/v1beta1
kind: InfisicalStaticSecret
metadata:
name: rclone-conf
namespace: backup-system
spec:
# Identidad compartida read-only (sin write grant) — el valor RCLONE_CONF
# se gestiona desde la UI de Infisical (proyecto homelab / prod / /backup-system).
infisicalAuthRef:
name: infisical-auth
namespace: infisical-operator
sources:
- projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab"
environmentSlug: prod
secretPath: /backup-system
syncOptions:
refreshInterval: 60s
targets:
- kind: Secret
name: rclone-conf-infisical
namespace: backup-system
creationPolicy: Owner
# El Secret lleva la key RCLONE_CONF con el contenido íntegro de rclone.conf;
# los CronJobs la montan como fichero /rclone/rclone.conf (items en el volume).
+11 -26
View File
@@ -11,7 +11,7 @@ metadata:
spec: spec:
finalizers: finalizers:
- kubernetes - kubernetes
---
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
@@ -25,7 +25,7 @@ metadata:
spec: spec:
finalizers: finalizers:
- kubernetes - kubernetes
---
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
@@ -39,7 +39,7 @@ metadata:
spec: spec:
finalizers: finalizers:
- kubernetes - kubernetes
---
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
@@ -53,7 +53,7 @@ metadata:
spec: spec:
finalizers: finalizers:
- kubernetes - kubernetes
---
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
@@ -67,7 +67,7 @@ metadata:
spec: spec:
finalizers: finalizers:
- kubernetes - kubernetes
---
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
@@ -77,7 +77,7 @@ metadata:
spec: spec:
finalizers: finalizers:
- kubernetes - kubernetes
---
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
@@ -91,7 +91,7 @@ metadata:
spec: spec:
finalizers: finalizers:
- kubernetes - kubernetes
---
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
@@ -105,7 +105,7 @@ metadata:
spec: spec:
finalizers: finalizers:
- kubernetes - kubernetes
---
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
@@ -119,7 +119,7 @@ metadata:
spec: spec:
finalizers: finalizers:
- kubernetes - kubernetes
---
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
@@ -133,21 +133,7 @@ metadata:
spec: spec:
finalizers: finalizers:
- kubernetes - kubernetes
---
apiVersion: v1
kind: Namespace
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: '{"apiVersion":"v1","kind":"Namespace","metadata":{"annotations":{},"name":"openclaw"},"spec":{},"status":{}}
'
labels:
kubernetes.io/metadata.name: openclaw
name: openclaw
spec:
finalizers:
- kubernetes
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
@@ -161,7 +147,7 @@ metadata:
spec: spec:
finalizers: finalizers:
- kubernetes - kubernetes
---
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
@@ -175,4 +161,3 @@ metadata:
spec: spec:
finalizers: finalizers:
- kubernetes - kubernetes
@@ -407,94 +407,6 @@ data:
"title": "n8n", "title": "n8n",
"type": "stat" "type": "stat"
}, },
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [
{
"options": {
"0": {
"color": "red",
"index": 0,
"text": "Down"
}
},
"type": "value"
},
{
"options": {
"from": 1,
"result": {
"color": "green",
"index": 1,
"text": "Running"
},
"to": null
},
"type": "range"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": null
},
{
"color": "green",
"value": 1
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 4,
"x": 4,
"y": 10
},
"id": 8,
"options": {
"colorMode": "background",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "12.4.2",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"expr": "sum(kube_pod_status_phase{namespace=\"openclaw\",phase=\"Running\"}) or vector(0)",
"instant": true,
"legendFormat": "openclaw",
"refId": "A"
}
],
"title": "openclaw",
"type": "stat"
},
{ {
"datasource": { "datasource": {
"type": "prometheus", "type": "prometheus",
@@ -1870,5 +1782,4 @@ data:
"uid": "chemavx-homelab-v1", "uid": "chemavx-homelab-v1",
"version": 3, "version": 3,
"weekStart": "" "weekStart": ""
} }
@@ -83,6 +83,47 @@ data:
conditions: conditions:
- evaluator: {params: [0], type: gt} - evaluator: {params: [0], type: gt}
- uid: homelab-backup-job-failed
title: "Backup job fallido"
condition: C
for: 1m
noDataState: OK
execErrState: Error
annotations:
summary: "🗄️ Backup FALLIDO: {{ $labels.job_name }}\nNamespace: {{ $labels.namespace }}"
description: "El job {{ $labels.job_name }} del sistema de backups ha fallado. Cubre los CronJobs backup, rclone-mega-backup y backup-verify."
labels:
severity: critical
isPaused: false
data:
- refId: A
relativeTimeRange: {from: 300, to: 0}
datasourceUid: prometheus
model:
editorMode: code
expr: "kube_job_status_failed{namespace=\"backup-system\"}"
instant: true
refId: A
- refId: B
relativeTimeRange: {from: 0, to: 0}
datasourceUid: "-100"
model:
type: reduce
refId: B
expression: A
reducer: last
settings:
mode: ""
- refId: C
relativeTimeRange: {from: 0, to: 0}
datasourceUid: "-100"
model:
type: threshold
refId: C
expression: B
conditions:
- evaluator: {params: [0], type: gt}
- uid: homelab-disk-high - uid: homelab-disk-high
title: "Disco > 80%" title: "Disco > 80%"
condition: C condition: C
+28
View File
@@ -3,6 +3,10 @@ kind: Deployment
metadata: metadata:
name: n8n name: n8n
namespace: n8n namespace: n8n
annotations:
# El operator de Infisical redespliega este Deployment cuando cambia
# n8n-secret-infisical (p.ej. rotación de GETXAPI_TOKEN) — sin kubectl manual.
secrets.infisical.com/auto-reload: "true"
spec: spec:
replicas: 1 replicas: 1
selector: selector:
@@ -34,6 +38,30 @@ spec:
secretKeyRef: secretKeyRef:
name: n8n-secret-infisical name: n8n-secret-infisical
key: encryption-key key: encryption-key
# Token getxapi (X autopost) — rotado 2026-07-09; el workflow lo lee
# con la expresión {{ $env.GETXAPI_TOKEN }}, nunca inline en el nodo.
- name: GETXAPI_TOKEN
valueFrom:
secretKeyRef:
name: n8n-secret-infisical
key: GETXAPI_TOKEN
# Token del bot @chemavx_bot (notificaciones) — rotado 2026-07-09.
# Lo consume el workflow "Health Check General" vía process.env,
# ya no hardcodeado. Fuente: Infisical homelab/prod/telegram.
- name: TELEGRAM_BOT_TOKEN
valueFrom:
secretKeyRef:
name: telegram-notify-infisical
key: TELEGRAM_BOT_TOKEN
- name: TELEGRAM_CHAT_ID
valueFrom:
secretKeyRef:
name: telegram-notify-infisical
key: TELEGRAM_CHAT_ID
# n8n ≥1.x bloquea $env en expresiones por defecto; sin esto el nodo
# HTTP del autopost no puede leer GETXAPI_TOKEN. Instancia single-user.
- name: N8N_BLOCK_ENV_ACCESS_IN_NODE
value: "false"
- name: N8N_HOST - name: N8N_HOST
value: n8n.chemavx.xyz value: n8n.chemavx.xyz
- name: N8N_PORT - name: N8N_PORT
+1 -3
View File
@@ -8,13 +8,12 @@ return await (async () => {
// ── Configuración ──────────────────────────────────────────────────────── // ── Configuración ────────────────────────────────────────────────────────
const TELEGRAM_TOKEN = '8611913802:AAFlrFtc0vYISOliO_W8B4c-W1ue0hG9Fio'; const TELEGRAM_TOKEN = process.env.TELEGRAM_BOT_TOKEN /* rotado: leer de env, no hardcodear */;
const TELEGRAM_CHAT = '5138407666'; const TELEGRAM_CHAT = '5138407666';
const RESTART_WEBHOOK = 'https://n8n.chemavx.xyz/webhook/uptime-kuma-restart'; const RESTART_WEBHOOK = 'https://n8n.chemavx.xyz/webhook/uptime-kuma-restart';
const SERVICES = [ const SERVICES = [
{ name: 'n8n', url: 'https://n8n.chemavx.xyz' }, { name: 'n8n', url: 'https://n8n.chemavx.xyz' },
{ name: 'openclaw', url: 'https://openclaw.chemavx.xyz' },
{ name: 'vaultwarden', url: 'https://vaultwarden.chemavx.xyz' }, { name: 'vaultwarden', url: 'https://vaultwarden.chemavx.xyz' },
{ name: 'grafana', url: 'https://grafana.chemavx.xyz' }, { name: 'grafana', url: 'https://grafana.chemavx.xyz' },
{ name: 'uptime', url: 'https://uptime.chemavx.xyz' }, { name: 'uptime', url: 'https://uptime.chemavx.xyz' },
@@ -22,7 +21,6 @@ return await (async () => {
{ name: 'home', url: 'https://home.chemavx.xyz' }, { name: 'home', url: 'https://home.chemavx.xyz' },
{ name: 'git', url: 'https://git.chemavx.xyz' }, { name: 'git', url: 'https://git.chemavx.xyz' },
{ name: 'argocd', url: 'https://argocd.chemavx.xyz' }, { name: 'argocd', url: 'https://argocd.chemavx.xyz' },
{ name: 'polymarket', url: 'https://polymarket.chemavx.xyz' },
{ name: 'ollama', url: 'https://ollama.chemavx.xyz/api/tags' }, { name: 'ollama', url: 'https://ollama.chemavx.xyz/api/tags' },
]; ];
-76
View File
@@ -1,76 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: openclaw
namespace: openclaw
spec:
replicas: 1
selector:
matchLabels:
app: openclaw
template:
metadata:
labels:
app: openclaw
spec:
nodeSelector:
kubernetes.io/hostname: chemavx-k8
serviceAccountName: openclaw-agent
securityContext:
runAsUser: 1000
fsGroup: 1000
initContainers:
- name: setup-kubectl
image: bitnami/kubectl:latest
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- |
cp $(which kubectl) /opt/kube/kubectl
chmod +x /opt/kube/kubectl
cat > /opt/kube/kubectl-ro << 'SCRIPT'
#!/bin/sh
DENIED="delete apply patch edit exec scale rollout drain cordon uncordon taint replace create annotate label"
if [ "$#" -eq 0 ]; then exec /opt/kube/kubectl "$@"; fi
SUBCMD="$1"
for d in $DENIED; do
if [ "$SUBCMD" = "$d" ]; then
echo "ERROR: \"$SUBCMD\" no permitido en modo solo lectura." >&2; exit 1
fi
done
exec /opt/kube/kubectl "$@"
SCRIPT
chmod +x /opt/kube/kubectl-ro
volumeMounts:
- name: kube-tools
mountPath: /opt/kube
securityContext:
runAsUser: 0
containers:
- name: openclaw
image: ghcr.io/openclaw/openclaw:2026.4.22
imagePullPolicy: IfNotPresent
ports:
- containerPort: 18789
env:
- name: NODE_OPTIONS
value: --max-old-space-size=1536
resources:
requests:
cpu: 100m
memory: 512Mi
limits:
cpu: "1"
memory: 2Gi
volumeMounts:
- name: data
mountPath: /home/node/.openclaw
- name: kube-tools
mountPath: /opt/kube
volumes:
- name: data
persistentVolumeClaim:
claimName: openclaw-pvc
- name: kube-tools
emptyDir: {}
-62
View File
@@ -1,62 +0,0 @@
# HOMELAB.md - Infraestructura del Homelab
Lee este archivo al inicio de cada sesión. Contiene el contexto completo del homelab.
## Cluster k3s
- **chemavx-k8** (master/control-plane): 192.168.1.225, Ubuntu 22.04.5 LTS, k3s v1.34.6
- CPU: 16 cores · RAM: ~32 GB
- GPU: NVIDIA GeForce RTX 3060 12 GB (CUDA, Ampere gfx8.6) — usada por Ollama
- **chemavx-n97** (worker): 192.168.1.238, Ubuntu 24.04.4 LTS, k3s v1.34.6
- CPU: 4 cores · RAM: ~12 GB
- Ingress: Traefik (LoadBalancer 192.168.1.225 + 192.168.1.238) · TLS: cert-manager + letsencrypt-prod
- Dominio: *.chemavx.xyz
- Storage: local-path (hostPath) — datos en chemavx-k8 NO accesibles desde chemavx-n97
## Servicios desplegados
| Servicio | Namespace | URL | Notas |
|-----------------|-----------------|------------------------------|-------|
| OpenClaw | openclaw | openclaw.chemavx.xyz | este agente, PVC /data |
| Ollama | ollama | ollama.chemavx.xyz | llama3.1-es:8b + qwen3-es:14b, RTX 3060 |
| Open WebUI | open-webui | chat.chemavx.xyz | interfaz web para Ollama |
| ArgoCD | argocd | argocd.chemavx.xyz | GitOps CD |
| Authentik | authentik | auth.chemavx.xyz | SSO, postgresql en chemavx-k8 |
| Gitea | gitea | git.chemavx.xyz | con Gitea Runner |
| Grafana | monitoring | grafana.chemavx.xyz | kube-prometheus-stack |
| Prometheus | monitoring | prometheus.chemavx.xyz | |
| Uptime Kuma | monitoring | status.chemavx.xyz | |
| n8n | n8n | n8n.chemavx.xyz | |
| Vaultwarden | vaultwarden | vaultwarden.chemavx.xyz | |
| Homarr | homarr | home.chemavx.xyz | dashboard |
| Polymarket Bot | polymarket-bot | polymarket.chemavx.xyz | api + bot + dashboard + postgres |
| Portfolio | portfolio | chemavx.xyz | web personal |
| Gitea | gitea | git.chemavx.xyz | |
## Modelos Ollama activos
| Modelo | Tipo | Rol en OpenClaw | Tok/s | Notas |
|------------------|---------|-----------------|-------|-------|
| qwen3-es:14b | custom | primary | ~34 | Modelfile español + /nothink, base qwen3:14b |
| llama3.1-es:8b | custom | fallback | ~50 | Modelfile español, base llama3.1:8b |
| qwen3:14b | base | disponible | 34.5 | RTX 3060 |
| qwen2.5:14b | base | disponible | - | |
| llama3.1:8b | base | disponible | - | |
## Namespaces activos
argocd · authentik · backup-system · cert-manager · cloudflare-ddns · gitea · gpu-operator · homarr · monitoring · n8n · ollama · open-webui · openclaw · polymarket-bot · portfolio · vaultwarden
## Manifiestos
Todos los manifiestos en: `/home/chemavx/k8s-manifests/<namespace>/`
## RBAC OpenClaw
ServiceAccount `openclaw-agent` con ClusterRole read-only:
- Puede: get/list/watch — pods, logs, services, nodes, namespaces, events, deployments, replicasets, statefulsets, daemonsets, ingresses
- NO puede: delete, patch, create, update — nada
## Reglas de operación
Ver SOUL.md sección "Homelab Safety" — **NUNCA** ejecutar comandos de administración del cluster sin confirmación explícita.
-65
View File
@@ -1,65 +0,0 @@
# SOUL.md
## Idioma
Español obligatorio. Todas las respuestas deben estar completamente en español.
Esto incluye saludos iniciales, respuestas normales, respuestas después de usar herramientas y respuestas tras mensajes de inicio o heartbeat.
No mezcles idiomas. No uses inglés salvo que el usuario lo pida explícitamente.
## Identidad
Eres ChemaVX Bot, el asistente personal y de homelab de ChemaVX.
No uses nombres alternativos. No te presentes como Luna ni como otra identidad.
## Estilo
Técnico, claro, directo y breve. Evita relleno. Haz primero lo útil.
## Inicio de conversación
Cuando se inicie una sesión nueva o tras reset, saluda en español en 1 o 2 frases como máximo y pregunta qué quiere hacer el usuario.
## Reglas para herramientas
Regla obligatoria para herramientas:
Nunca devuelvas la salida de una herramienta directamente.
Siempre debes:
1. Interpretar el resultado
2. Traducirlo al español si está en inglés
3. Responder al usuario con una explicación clara y breve
La respuesta final SIEMPRE debe estar en español, aunque la herramienta devuelva contenido en inglés.
Las herramientas no hablan al usuario.
Tú eres quien responde.
Está prohibido devolver texto de herramientas sin procesar.
## Acceso al cluster Kubernetes
Para consultar el cluster usa siempre el comando `exec` con `/opt/kube/kubectl-ro`.
El binario `/opt/kube/kubectl-ro` es el wrapper de kubectl de solo lectura.
El KUBECONFIG ya está configurado en el entorno — no necesitas pasar parámetros adicionales.
Ejemplos:
- Listar pods: `exec``/opt/kube/kubectl-ro get pods -A`
- Ver nodos: `exec``/opt/kube/kubectl-ro get nodes`
- Logs: `exec``/opt/kube/kubectl-ro logs -n <ns> <pod> --tail=50`
- Describir: `exec``/opt/kube/kubectl-ro describe pod -n <ns> <pod>`
## Seguridad Homelab
Para consultas de lectura (`get`, `describe`, `logs`, `top`, `version`):
→ Ejecuta directamente con `/opt/kube/kubectl-ro`. NO pidas permiso.
Para acciones de modificación del cluster (`apply`, `delete`, `patch`, `edit`, `scale`, `rollout restart`, `drain`, `cordon`, etc.):
→ NUNCA ejecutes sin confirmación explícita del usuario.
→ Muestra el comando exacto y espera "sí" explícito antes de ejecutar.
Incluye también: `helm install/upgrade/uninstall`, `kubeadm`, `k3s`.
## Continuidad
Estos archivos son tu memoria. Léelos al inicio de cada sesión. Actualízalos según avances.
-38
View File
@@ -1,38 +0,0 @@
# TOOLS.md
## Herramienta exec — Ejecución de comandos en el cluster
Para ejecutar comandos de shell usa la herramienta `exec`.
**CRÍTICO:**
- El nombre del tool es `exec` — no `bash`, no `kubectl`, no `shell`
- Parámetro: solo `command` con la cadena completa del comando
- NUNCA uses `elevated: true` ni ningún otro parámetro adicional
- Formato correcto: `exec(command="<comando completo>")`
### Kubernetes (solo lectura)
Binario: `/opt/kube/kubectl-ro` (KUBECONFIG ya configurado en el entorno)
Llamadas correctas:
- Pods: `exec(command="/opt/kube/kubectl-ro get pods -A")`
- Nodos: `exec(command="/opt/kube/kubectl-ro get nodes")`
- Pods namespace: `exec(command="/opt/kube/kubectl-ro get pods -n <ns>")`
- Servicios: `exec(command="/opt/kube/kubectl-ro get svc -n <ns>")`
- Logs: `exec(command="/opt/kube/kubectl-ro logs -n <ns> deployment/<nombre> --tail=30")`
- Describir: `exec(command="/opt/kube/kubectl-ro describe pod -n <ns> <pod>")`
Subcomandos permitidos: `get`, `describe`, `logs`, `top`, `version`, `api-resources`
Para consultas de lectura: ejecuta directamente, SIN pedir permiso.
### Regla de uso
Ante cualquier pregunta sobre el cluster:
1. Llama a `exec(command="/opt/kube/kubectl-ro <subcomando> ...")`
2. Interpreta el resultado
3. Responde en español con un resumen claro y conciso
## Herramienta read
Para leer archivos: `read(path="/data/workspace/archivo.md")`
-10
View File
@@ -1,10 +0,0 @@
#!/bin/bash
DENIED="delete apply patch edit exec scale rollout drain cordon uncordon taint replace create annotate label"
if [ "$#" -eq 0 ]; then exec /opt/kube/kubectl "$@"; fi
SUBCMD="$1"
for d in $DENIED; do
if [ "$SUBCMD" = "$d" ]; then
echo "ERROR: \"$SUBCMD\" no permitido en modo solo lectura." >&2; exit 1
fi
done
exec /opt/kube/kubectl "$@"
-220
View File
@@ -1,220 +0,0 @@
{
"meta": {
"lastTouchedVersion": "2026.4.12",
"lastTouchedAt": "2026-04-20T15:00:00.000Z"
},
"browser": {
"cdpUrl": "http://localhost:9222"
},
"models": {
"mode": "merge",
"providers": {
"ollama": {
"baseUrl": "http://ollama.ollama.svc.cluster.local:11434",
"api": "ollama",
"apiKey": "ollama-local",
"models": [
{
"id": "qwen3-es:14b",
"name": "Qwen3 ES 14B",
"reasoning": false,
"input": [
"text"
],
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
},
"contextWindow": 128000,
"contextTokens": 32768,
"maxTokens": 8192
},
{
"id": "qwen3:14b",
"name": "Qwen3 14B",
"reasoning": false,
"input": [
"text"
],
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
},
"contextWindow": 128000,
"contextTokens": 32768,
"maxTokens": 8192
},
{
"id": "llama3.1-es:8b",
"name": "Llama 3.1 ES 8B",
"reasoning": false,
"input": [
"text"
],
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
},
"contextWindow": 128000,
"contextTokens": 16384,
"maxTokens": 4096
},
{
"id": "llama3.1:8b",
"name": "Llama 3.1 8B",
"reasoning": false,
"input": [
"text"
],
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
},
"contextWindow": 128000,
"contextTokens": 16384,
"maxTokens": 4096
}
]
}
}
},
"agents": {
"defaults": {
"userTimezone": "Europe/Madrid",
"timeFormat": "24",
"model": {
"primary": "ollama/qwen3-es:14b",
"fallbacks": [
"ollama/llama3.1-es:8b"
]
},
"timeoutSeconds": 600,
"heartbeat": {
"every": "6h"
},
"workspace": "/data/workspace",
"contextInjection": "continuation-skip",
"bootstrapMaxChars": 4000,
"bootstrapTotalMaxChars": 12000,
"bootstrapPromptTruncationWarning": "once",
"compaction": {
"mode": "safeguard"
},
"startupContext": {
"enabled": true,
"applyOn": [
"new",
"reset"
],
"dailyMemoryDays": 1,
"maxFileBytes": 8192,
"maxFileChars": 800,
"maxTotalChars": 1600
},
"contextPruning": {
"mode": "cache-ttl",
"ttl": "1h",
"keepLastAssistants": 3,
"softTrimRatio": 0.3,
"hardClearRatio": 0.5,
"minPrunableToolChars": 30000,
"softTrim": {
"maxChars": 2500,
"headChars": 1000,
"tailChars": 1000
},
"hardClear": {
"enabled": true,
"placeholder": "[Old tool result content cleared]"
},
"tools": {
"deny": [
"browser",
"canvas"
]
}
},
"memorySearch": {
"enabled": true,
"provider": "ollama"
}
}
},
"commands": {
"native": "auto",
"nativeSkills": "auto",
"restart": true,
"ownerDisplay": "raw"
},
"channels": {
"telegram": {
"enabled": true,
"dmPolicy": "pairing",
"botToken": "8611913802:AAFlrFtc0vYISOliO_W8B4c-W1ue0hG9Fio",
"groupPolicy": "allowlist",
"streaming": {
"mode": "partial"
}
}
},
"gateway": {
"mode": "local",
"bind": "lan",
"controlUi": {
"allowedOrigins": [
"http://localhost:18789",
"http://192.168.1.92:18789",
"https://openclaw.chemavx.xyz"
]
},
"auth": {
"mode": "token",
"token": "9242ad46f6604770bb98ed2bda7bded7546f21c5233f1b24",
"rateLimit": {
"maxAttempts": 100,
"lockoutMs": 0
}
},
"trustedProxies": [
"10.42.0.0/16"
]
},
"plugins": {
"entries": {
"ollama": {
"enabled": true
},
"browser": {
"enabled": true
}
}
},
"tools": {
"profile": "coding",
"deny": [
"edit",
"write",
"apply_patch"
],
"exec": {
"host": "gateway",
"security": "allowlist",
"ask": "off",
"pathPrepend": [
"/opt/kube"
],
"timeoutSec": 30
},
"elevated": {
"enabled": true
}
}
}
-22
View File
@@ -1,22 +0,0 @@
apiVersion: secrets.infisical.com/v1beta1
kind: InfisicalStaticSecret
metadata:
name: telegram-notify
namespace: openclaw
spec:
# Shared, read-only machine identity (no write grant, no PushSecret).
infisicalAuthRef:
name: infisical-auth
namespace: infisical-operator
sources:
- projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab"
environmentSlug: prod
secretPath: /telegram # shared across all 5 namespaces
syncOptions:
refreshInterval: 60s
targets:
- kind: Secret
name: telegram-notify-infisical # parallel name — old telegram-notify stays live until 3c
namespace: openclaw
creationPolicy: Owner
# key passthrough: synced Secret carries TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID verbatim
-25
View File
@@ -1,25 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: openclaw
namespace: openclaw
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.entrypoints: websecure
spec:
ingressClassName: traefik
rules:
- host: openclaw.chemavx.xyz
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: openclaw
port:
number: 18789
tls:
- hosts:
- openclaw.chemavx.xyz
secretName: openclaw-tls
-4
View File
@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: openclaw
-54
View File
@@ -1,54 +0,0 @@
apiVersion: batch/v1
kind: Job
metadata:
name: postsync-smoke-test
namespace: openclaw
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
ttlSecondsAfterFinished: 600
backoffLimit: 0
template:
spec:
restartPolicy: Never
containers:
- name: smoke-test
image: curlimages/curl:latest
env:
- name: TELEGRAM_BOT_TOKEN
valueFrom:
secretKeyRef:
name: telegram-notify-infisical
key: TELEGRAM_BOT_TOKEN
- name: TELEGRAM_CHAT_ID
valueFrom:
secretKeyRef:
name: telegram-notify-infisical
key: TELEGRAM_CHAT_ID
command: ["/bin/sh", "-c"]
args:
- |
set -e
BASE="http://openclaw.openclaw.svc.cluster.local:18789"
notify() {
curl -s --max-time 10 -X POST \
"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d "chat_id=${TELEGRAM_CHAT_ID}" \
--data-urlencode "text=$1" \
> /dev/null || true
}
fail() {
notify "[FAIL] openclaw PostSync: $1"
exit 1
}
echo "--- [1/1] GET / (HTTP 200) ---"
curl -sfL --max-time 15 --retry 3 --retry-delay 5 --retry-all-errors \
"$BASE/" > /dev/null || fail "GET / returned non-200"
echo "OK"
notify "[OK] openclaw PostSync passed"
echo "=== openclaw: all smoke tests passed ==="
-15
View File
@@ -1,15 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
annotations:
# Never let ArgoCD prune this PVC: it holds persistent app data.
argocd.argoproj.io/sync-options: Prune=false
name: openclaw-pvc
namespace: openclaw
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 5Gi
-36
View File
@@ -1,36 +0,0 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: openclaw-agent
namespace: openclaw
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: openclaw-agent-readonly
rules:
- apiGroups: [""]
resources: [pods, pods/log, services, nodes, namespaces, events]
verbs: [get, list, watch]
- apiGroups: [apps]
resources: [deployments, replicasets, statefulsets, daemonsets]
verbs: [get, list, watch]
- apiGroups: [networking.k8s.io]
resources: [ingresses]
verbs: [get, list, watch]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: openclaw-agent-readonly-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: openclaw-agent-readonly
subjects:
- kind: ServiceAccount
name: openclaw-agent
namespace: openclaw
-12
View File
@@ -1,12 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: openclaw
namespace: openclaw
spec:
selector:
app: openclaw
ports:
- port: 18789
targetPort: 18789
type: ClusterIP
-14
View File
@@ -1,14 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: bot-config
namespace: polymarket-bot
# Non-secret config split out of the old bot-secrets Secret. PAPER_* are plain
# operating params; WALLET_PRIVATE_KEY and COINGECKO_API_KEY are kept here as
# empty placeholders (preserving the "present-but-empty" env semantics the bot
# was started with — avoids a KeyError if the code reads them unconditionally).
data:
PAPER_BANKROLL: "10000"
PAPER_MODE: "true"
WALLET_PRIVATE_KEY: ""
COINGECKO_API_KEY: ""
@@ -1,55 +0,0 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: metrics-retention
namespace: polymarket-bot
spec:
# Daily at 00:10 UTC: collapse every past UTC day in metrics_daily to its
# last snapshot (the "daily close"). The bot writes one snapshot per cycle
# (~1,300/day); only the close of past days is read back (DISTINCT ON in
# get_metrics_history / get_daily_pnl_closes), so intraday rows of past
# days are dead weight. Today's rows are never touched: the latest one
# feeds /api/summary and the close is still evolving.
schedule: "10 0 * * *"
suspend: true
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 2
template:
spec:
restartPolicy: Never
containers:
- name: prune
image: postgres:16-alpine
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: bot-secrets-infisical
key: DATABASE_URL
command:
- /bin/sh
- -c
- |
set -e
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c "
DELETE FROM metrics_daily m
USING (
SELECT timestamp::date AS day, MAX(timestamp) AS keep_ts
FROM metrics_daily
WHERE timestamp::date < CURRENT_DATE
GROUP BY 1
) k
WHERE m.timestamp::date = k.day
AND m.timestamp < k.keep_ts;
"
psql "$DATABASE_URL" -c "VACUUM ANALYZE metrics_daily;"
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 128Mi
-47
View File
@@ -1,47 +0,0 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: outcomes-joiner
namespace: polymarket-bot
spec:
# Daily at 00:30 UTC (after metrics-retention at 00:10, no overlap): fetch
# UMA-final resolutions for archived markets from the Gamma API into
# market_outcomes, then print the calibration report (Brier/log-loss of the
# model vs the market price). Replay R2 joiner — analysis only: reads the
# signals archive, writes only market_outcomes, never touches trading
# tables. Safe to also run by hand any time (kubectl create job --from=...):
# the upsert is idempotent per market_id (ON CONFLICT DO UPDATE) and the
# fetch only queries markets that still lack an outcome.
schedule: "30 0 * * *"
suspend: true
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 2
# A hung Gamma call must not block tomorrow's run through Forbid
activeDeadlineSeconds: 900
template:
spec:
restartPolicy: OnFailure
imagePullSecrets:
- name: gitea-registry-infisical
containers:
- name: outcomes
# Same image as the bot deployment; CI bumps this tag on every
# bot image build (see polymarket-bot repo .gitea/workflows/ci.yml)
image: git.chemavx.xyz/chemavx/polymarket-bot:2b326ad5
imagePullPolicy: Always
command: ["python3", "-m", "bot.outcomes"]
envFrom:
- secretRef:
name: bot-secrets-infisical
- configMapRef:
name: bot-config
resources:
requests:
cpu: 25m
memory: 64Mi
limits:
memory: 256Mi
-57
View File
@@ -1,57 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: polymarket-bot
spec:
progressDeadlineSeconds: 600
replicas: 0
revisionHistoryLimit: 10
selector:
matchLabels:
app: api
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
app: api
spec:
imagePullSecrets:
- name: gitea-registry-infisical
containers:
- command:
- uvicorn
- api.main:app
- --host
- 0.0.0.0
- --port
- "8000"
envFrom:
- secretRef:
name: bot-secrets-infisical
- configMapRef:
name: bot-config
image: git.chemavx.xyz/chemavx/polymarket-bot-api:7804d25c
imagePullPolicy: Always
name: api
ports:
- containerPort: 8000
protocol: TCP
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
-51
View File
@@ -1,51 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: bot
namespace: polymarket-bot
spec:
progressDeadlineSeconds: 600
replicas: 0
revisionHistoryLimit: 10
selector:
matchLabels:
app: bot
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
app: bot
spec:
imagePullSecrets:
- name: gitea-registry-infisical
containers:
- command:
- python3
- -m
- bot.main
envFrom:
- secretRef:
name: bot-secrets-infisical
- configMapRef:
name: bot-config
image: git.chemavx.xyz/chemavx/polymarket-bot:7804d25c
imagePullPolicy: Always
name: bot
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
-45
View File
@@ -1,45 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: dashboard
namespace: polymarket-bot
spec:
progressDeadlineSeconds: 600
replicas: 0
revisionHistoryLimit: 10
selector:
matchLabels:
app: dashboard
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
app: dashboard
spec:
imagePullSecrets:
- name: gitea-registry-infisical
containers:
- image: git.chemavx.xyz/chemavx/polymarket-bot-dashboard:7804d25c
imagePullPolicy: Always
name: dashboard
ports:
- containerPort: 80
protocol: TCP
resources:
limits:
cpu: 200m
memory: 128Mi
requests:
cpu: 50m
memory: 64Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
-30
View File
@@ -1,30 +0,0 @@
apiVersion: secrets.infisical.com/v1beta1
kind: InfisicalStaticSecret
metadata:
name: bot-secrets
namespace: polymarket-bot
spec:
# Shared, read-only machine identity (no write grant, no PushSecret).
infisicalAuthRef:
name: infisical-auth
namespace: infisical-operator
sources:
# App-specific secrets (4): DATABASE_URL, POLYMARKET_API_KEY/SECRET/PASSPHRASE
- projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab"
environmentSlug: prod
secretPath: /polymarket-bot
# Telegram (2): TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID — reused from the shared
# /telegram path (deduplicated; same token as the other 5 namespaces).
- projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0
environmentSlug: prod
secretPath: /telegram
syncOptions:
refreshInterval: 60s
targets:
- kind: Secret
name: bot-secrets-infisical # parallel name — old bot-secrets stays live until 3c
namespace: polymarket-bot
creationPolicy: Owner
# key passthrough: synced Secret carries all 6 keys verbatim.
# PAPER_* and the empty WALLET_PRIVATE_KEY/COINGECKO_API_KEY are config,
# not secrets — they live in the bot-config ConfigMap, not here.
@@ -1,24 +0,0 @@
apiVersion: secrets.infisical.com/v1beta1
kind: InfisicalStaticSecret
metadata:
name: telegram-notify
namespace: polymarket-bot
spec:
# Shared, read-only machine identity (no write grant, no PushSecret).
# NOTE: this migrates only the STANDALONE telegram-notify secret. The same
# Telegram token also embedded in bot-secrets is migration #3 — not touched here.
infisicalAuthRef:
name: infisical-auth
namespace: infisical-operator
sources:
- projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab"
environmentSlug: prod
secretPath: /telegram # shared across all 5 namespaces
syncOptions:
refreshInterval: 60s
targets:
- kind: Secret
name: telegram-notify-infisical # parallel name — old telegram-notify stays live until 3c
namespace: polymarket-bot
creationPolicy: Owner
# key passthrough: synced Secret carries TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID verbatim
@@ -1,33 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: polymarket-bot
namespace: polymarket-bot
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
spec:
ingressClassName: traefik
rules:
- host: polymarket.chemavx.xyz
http:
paths:
- backend:
service:
name: api
port:
number: 8000
path: /api
pathType: Prefix
- backend:
service:
name: dashboard
port:
number: 80
path: /
pathType: Prefix
tls:
- hosts:
- polymarket.chemavx.xyz
secretName: polymarket-tls
-25
View File
@@ -1,25 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
annotations:
# Never let ArgoCD prune this PVC: it holds the paper-trading postgres data.
argocd.argoproj.io/sync-options: Prune=false
pv.kubernetes.io/bind-completed: 'yes'
pv.kubernetes.io/bound-by-controller: 'yes'
volume.beta.kubernetes.io/storage-provisioner: rancher.io/local-path
volume.kubernetes.io/selected-node: chemavx-k8
volume.kubernetes.io/storage-provisioner: rancher.io/local-path
labels:
app: postgres
name: data-postgres-0
namespace: polymarket-bot
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: local-path
volumeMode: Filesystem
volumeName: pvc-327bbc6d-dcc6-46b4-9d78-db950f71eb98
-22
View File
@@ -1,22 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: api
namespace: polymarket-bot
spec:
clusterIP: 10.43.201.4
clusterIPs:
- 10.43.201.4
internalTrafficPolicy: Cluster
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- port: 8000
protocol: TCP
targetPort: 8000
selector:
app: api
sessionAffinity: None
type: ClusterIP
-22
View File
@@ -1,22 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: dashboard
namespace: polymarket-bot
spec:
clusterIP: 10.43.62.6
clusterIPs:
- 10.43.62.6
internalTrafficPolicy: Cluster
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: dashboard
sessionAffinity: None
type: ClusterIP
-22
View File
@@ -1,22 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: polymarket-bot
spec:
clusterIP: 10.43.238.53
clusterIPs:
- 10.43.238.53
internalTrafficPolicy: Cluster
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- port: 5432
protocol: TCP
targetPort: 5432
selector:
app: postgres
sessionAffinity: None
type: ClusterIP
-76
View File
@@ -1,76 +0,0 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: polymarket-bot
spec:
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain
whenScaled: Retain
podManagementPolicy: OrderedReady
replicas: 0
revisionHistoryLimit: 10
selector:
matchLabels:
app: postgres
serviceName: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- env:
- name: POSTGRES_USER
value: bot
- name: POSTGRES_PASSWORD
value: bot
- name: POSTGRES_DB
value: polymarket
image: postgres:16-alpine
imagePullPolicy: IfNotPresent
name: postgres
ports:
- containerPort: 5432
protocol: TCP
readinessProbe:
exec:
command:
- pg_isready
- -U
- bot
failureThreshold: 3
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 1
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: data
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
updateStrategy:
rollingUpdate:
partition: 0
type: RollingUpdate
volumeClaimTemplates:
- apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
volumeMode: Filesystem
status:
phase: Pending
+320 -142
View File
@@ -1,147 +1,325 @@
apiVersion: v1 apiVersion: v1
data: data:
index.html: "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" index.html: |
/>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" <!DOCTYPE html>
/>\n <title>ChemaVX</title>\n <style>\n *, *::before, *::after { box-sizing: <html lang="en">
border-box; margin: 0; padding: 0; }\n\n :root {\n --bg: #0d1117;\n <head>
\ --surface: #161b22;\n --border: #30363d;\n --text: #e6edf3;\n <meta charset="UTF-8" />
\ --muted: #8b949e;\n --accent: #58a6ff;\n --green: #3fb950;\n <meta name="viewport" content="width=device-width, initial-scale=1.0" />
\ --red: #f85149;\n --yellow: #d29922;\n --purple: #bc8cff;\n <meta name="description" content="ChemaVX — homelab k3s self-hosted: GitOps con ArgoCD, secrets con Infisical, backups offsite duales y publicación bilingüe." />
\ }\n\n body {\n background: var(--bg);\n color: var(--text);\n <title>ChemaVX</title>
\ font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, <style>
Arial, sans-serif;\n min-height: 100vh;\n display: flex;\n flex-direction: *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
column;\n align-items: center;\n padding: 3rem 1.5rem 4rem;\n }\n\n
\ /* ── Header ─────────────────────────────────── */\n header {\n text-align: :root {
center;\n margin-bottom: 3.5rem;\n }\n .avatar {\n width: 80px; --bg: #0d1117;
height: 80px;\n border-radius: 50%;\n background: linear-gradient(135deg, --surface: #161b22;
#1f6feb 0%, #bc8cff 100%);\n display: flex; align-items: center; justify-content: --border: #30363d;
center;\n font-size: 2rem; font-weight: 700; color: #fff;\n margin: --text: #e6edf3;
0 auto 1.25rem;\n box-shadow: 0 0 0 3px var(--border);\n }\n h1 { font-size: --muted: #8b949e;
2rem; font-weight: 700; letter-spacing: -0.5px; }\n .tagline {\n color: --accent: #58a6ff;
var(--muted); font-size: 0.95rem; margin-top: 0.4rem;\n }\n .dot { display: --green: #3fb950;
inline-block; width: 8px; height: 8px;\n border-radius: 50%; background: --red: #f85149;
var(--green);\n margin-right: 6px; animation: pulse 2s infinite; }\n --yellow: #d29922;
\ @keyframes pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: --purple: #bc8cff;
0.4; }\n }\n\n /* ── Section title ───────────────────────────── */\n .section-title }
{\n font-size: 0.7rem; font-weight: 600; letter-spacing: 0.08em;\n text-transform:
uppercase; color: var(--muted);\n margin-bottom: 0.875rem;\n }\n\n /* body {
── Services grid ───────────────────────────── */\n .services-grid {\n display: background: var(--bg);
grid;\n grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));\n gap: color: var(--text);
0.75rem;\n width: 100%; max-width: 720px;\n margin-bottom: 2.5rem;\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
\ }\n .service-card {\n background: var(--surface);\n border: 1px min-height: 100vh;
solid var(--border);\n border-radius: 10px;\n padding: 1rem 1.1rem;\n display: flex;
\ text-decoration: none;\n color: var(--text);\n transition: border-color flex-direction: column;
0.15s, transform 0.15s, box-shadow 0.15s;\n display: flex; align-items: center; align-items: center;
gap: 0.65rem;\n }\n .service-card:hover {\n border-color: var(--accent);\n padding: 3rem 1.5rem 4rem;
\ transform: translateY(-2px);\n box-shadow: 0 4px 16px rgba(88,166,255,0.1);\n }
\ }\n .service-icon { font-size: 1.3rem; flex-shrink: 0; }\n .service-name
{ font-size: 0.9rem; font-weight: 500; }\n\n /* ── Polymarket card ─────────────────────────── /* ── Header ─────────────────────────────────── */
*/\n .poly-wrapper { width: 100%; max-width: 720px; }\n .poly-card {\n background: header {
var(--surface);\n border: 1px solid var(--border);\n border-radius: text-align: center;
12px;\n padding: 1.5rem;\n margin-bottom: 2.5rem;\n }\n .poly-header margin-bottom: 3rem;
{\n display: flex; align-items: center; justify-content: space-between;\n }
\ margin-bottom: 1.25rem;\n }\n .poly-title {\n display: flex; .avatar {
align-items: center; gap: 0.5rem;\n font-size: 0.95rem; font-weight: 600;\n width: 80px; height: 80px;
\ }\n .poly-badge {\n font-size: 0.65rem; font-weight: 600; letter-spacing: border-radius: 50%;
0.04em;\n padding: 2px 7px; border-radius: 20px;\n background: rgba(188,140,255,0.15); background: linear-gradient(135deg, #1f6feb 0%, #bc8cff 100%);
color: var(--purple);\n border: 1px solid rgba(188,140,255,0.3);\n }\n display: flex; align-items: center; justify-content: center;
\ #poly-updated {\n font-size: 0.72rem; color: var(--muted);\n }\n\n font-size: 2rem; font-weight: 700; color: #fff;
\ .metrics-grid {\n display: grid;\n grid-template-columns: repeat(2, margin: 0 auto 1.25rem;
1fr);\n gap: 0.75rem;\n }\n @media (min-width: 480px) {\n .metrics-grid box-shadow: 0 0 0 3px var(--border);
{ grid-template-columns: repeat(4, 1fr); }\n }\n .metric {\n background: }
var(--bg);\n border: 1px solid var(--border);\n border-radius: 8px;\n h1 { font-size: 2rem; font-weight: 700; letter-spacing: -0.5px; }
\ padding: 0.875rem 1rem;\n }\n .metric-label {\n font-size: 0.68rem; .tagline {
color: var(--muted);\n text-transform: uppercase; letter-spacing: 0.06em;\n color: var(--muted); font-size: 0.95rem; margin-top: 0.4rem;
\ margin-bottom: 0.35rem;\n }\n .metric-value {\n font-size: 1.35rem; }
font-weight: 700; font-variant-numeric: tabular-nums;\n transition: color .dot { display: inline-block; width: 8px; height: 8px;
0.3s;\n }\n .metric-value.positive { color: var(--green); }\n .metric-value.negative border-radius: 50%; background: var(--green);
{ color: var(--red); }\n .metric-value.neutral { color: var(--text); }\n .metric-value.loading margin-right: 6px; animation: pulse 2s infinite; }
\ { color: var(--muted); font-size: 1rem; }\n\n /* ── Footer ──────────────────────────────────── @keyframes pulse {
*/\n footer {\n color: var(--muted); font-size: 0.75rem; text-align: center;\n 0%, 100% { opacity: 1; }
\ width: 100%; max-width: 720px;\n border-top: 1px solid var(--border);\n 50% { opacity: 0.4; }
\ padding-top: 1.25rem;\n }\n footer a { color: var(--accent); text-decoration: }
none; }\n footer a:hover { text-decoration: underline; }\n </style>\n</head>\n<body>\n\n .badges {
\ <header>\n <div class=\"avatar\">C</div>\n <h1>ChemaVX</h1>\n <p class=\"tagline\">\n display: flex; flex-wrap: wrap; gap: 0.5rem;
\ <span class=\"dot\"></span>Homelab · k3s · Self-hosted\n </p>\n </header>\n\n justify-content: center; margin-top: 1.1rem;
\ <!-- Services -->\n <div style=\"width:100%;max-width:720px;margin-bottom:2.5rem;\">\n max-width: 560px;
\ <p class=\"section-title\">Services</p>\n <div class=\"services-grid\">\n }
\ <a class=\"service-card\" href=\"https://grafana.chemavx.xyz\" target=\"_blank\" .badge {
rel=\"noopener\">\n <span class=\"service-icon\">\U0001F4CA</span>\n <span font-size: 0.7rem; color: var(--muted);
class=\"service-name\">Grafana</span>\n </a>\n <a class=\"service-card\" border: 1px solid var(--border); border-radius: 20px;
href=\"https://n8n.chemavx.xyz\" target=\"_blank\" rel=\"noopener\">\n <span padding: 3px 11px; background: var(--surface);
class=\"service-icon\">⚙️</span>\n <span class=\"service-name\">n8n</span>\n white-space: nowrap;
\ </a>\n <a class=\"service-card\" href=\"https://home.chemavx.xyz\" }
target=\"_blank\" rel=\"noopener\">\n <span class=\"service-icon\">\U0001F3E0</span>\n .badge b { color: var(--text); font-weight: 600; }
\ <span class=\"service-name\">Homarr</span>\n </a>\n <a class=\"service-card\"
href=\"https://polymarket.chemavx.xyz\" target=\"_blank\" rel=\"noopener\">\n /* ── Section title ───────────────────────────── */
\ <span class=\"service-icon\">\U0001F4C8</span>\n <span class=\"service-name\">Polymarket .section-title {
Bot</span>\n </a>\n <a class=\"service-card\" href=\"https://chat.chemavx.xyz\" font-size: 0.7rem; font-weight: 600; letter-spacing: 0.08em;
target=\"_blank\" rel=\"noopener\">\n <span class=\"service-icon\">\U0001F916</span>\n text-transform: uppercase; color: var(--muted);
\ <span class=\"service-name\">Open WebUI</span>\n </a>\n <a class=\"service-card\" margin-bottom: 0.875rem;
href=\"https://git.chemavx.xyz\" target=\"_blank\" rel=\"noopener\">\n <span }
class=\"service-icon\">\U0001F419</span>\n <span class=\"service-name\">Gitea</span>\n
\ </a>\n <a class=\"service-card\" href=\"https://argocd.chemavx.xyz\" /* ── Services grid ───────────────────────────── */
target=\"_blank\" rel=\"noopener\">\n <span class=\"service-icon\">\U0001F504</span>\n .services-grid {
\ <span class=\"service-name\">ArgoCD</span>\n </a>\n <a class=\"service-card\" display: grid;
href=\"https://vaultwarden.chemavx.xyz\" target=\"_blank\" rel=\"noopener\">\n grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
\ <span class=\"service-icon\">\U0001F510</span>\n <span class=\"service-name\">Vaultwarden</span>\n gap: 0.75rem;
\ </a>\n </div>\n </div>\n\n <!-- Projects -->\n <div style=\"width:100%;max-width:720px;margin-bottom:2.5rem;\">\n width: 100%; max-width: 720px;
\ <p class=\"section-title\">Projects</p>\n <div class=\"services-grid\">\n margin-bottom: 2.5rem;
\ <a class=\"service-card\" href=\"/ze\">\n <span class=\"service-icon\">\U0001F6F8</span>\n }
\ <span class=\"service-name\">ze</span>\n </a>\n </div>\n </div>\n\n .service-card {
\ <!-- Polymarket live metrics -->\n <div class=\"poly-wrapper\">\n <p class=\"section-title\">Polymarket background: var(--surface);
Bot — Live</p>\n <div class=\"poly-card\">\n <div class=\"poly-header\">\n border: 1px solid var(--border);
\ <div class=\"poly-title\">\n <span>\U0001F4C8</span>\n <span>Portfolio border-radius: 10px;
Summary</span>\n <span class=\"poly-badge\" id=\"poly-mode\">PAPER</span>\n padding: 0.9rem 1.1rem;
\ </div>\n <span id=\"poly-updated\">Loading…</span>\n </div>\n text-decoration: none;
\ <div class=\"metrics-grid\">\n <div class=\"metric\">\n <div color: var(--text);
class=\"metric-label\">P&amp;L</div>\n <div class=\"metric-value loading\" transition: border-color 0.15s, transform 0.15s, box-shadow 0.15s;
id=\"m-pnl\">—</div>\n </div>\n <div class=\"metric\">\n <div display: flex; align-items: center; gap: 0.65rem;
class=\"metric-label\">Win Rate</div>\n <div class=\"metric-value loading\" }
id=\"m-winrate\">—</div>\n </div>\n <div class=\"metric\">\n <div .service-card:hover {
class=\"metric-label\">Deployed</div>\n <div class=\"metric-value loading\" border-color: var(--accent);
id=\"m-deployed\">—</div>\n </div>\n <div class=\"metric\">\n <div transform: translateY(-2px);
class=\"metric-label\">Trades</div>\n <div class=\"metric-value loading\" box-shadow: 0 4px 16px rgba(88,166,255,0.1);
id=\"m-trades\">—</div>\n </div>\n <div class=\"metric\">\n <div }
class=\"metric-label\">Bankroll</div>\n <div class=\"metric-value loading\" .service-icon { font-size: 1.3rem; flex-shrink: 0; }
id=\"m-bankroll\">—</div>\n </div>\n <div class=\"metric\">\n <div .service-name { font-size: 0.9rem; font-weight: 500; line-height: 1.25; }
class=\"metric-label\">Sharpe</div>\n <div class=\"metric-value loading\" .service-sub { font-size: 0.68rem; color: var(--muted); margin-top: 1px; }
id=\"m-sharpe\">—</div>\n </div>\n <div class=\"metric\">\n <div
class=\"metric-label\">Calibration</div>\n <div class=\"metric-value /* ── Polymarket card ─────────────────────────── */
loading\" id=\"m-calibration\">—</div>\n </div>\n <div class=\"metric\">\n .poly-wrapper { width: 100%; max-width: 720px; }
\ <div class=\"metric-label\">Status</div>\n <div class=\"metric-value .poly-card {
loading\" id=\"m-status\">—</div>\n </div>\n </div>\n </div>\n background: var(--surface);
\ </div>\n\n <footer>\n <p>\n Hosted on <strong>k3s</strong> &mdash;\n border: 1px solid var(--border);
\ <a href=\"https://grafana.chemavx.xyz/d/chemavx-homelab-v1/chemavx-homelab-overview\" border-radius: 12px;
target=\"_blank\">Cluster Dashboard</a>\n &mdash; <a href=\"https://git.chemavx.xyz\" padding: 1.5rem;
target=\"_blank\">Gitea</a>\n </p>\n </footer>\n\n <script>\n const API margin-bottom: 2.5rem;
= 'https://polymarket.chemavx.xyz/api/summary';\n const fmt$ = v => v == null }
? '—' : '$' + v.toFixed(2);\n const fmtPct = v => v == null ? '—' : (v * 100).toFixed(1) .poly-header {
+ '%';\n const fmtN = v => v == null ? '—' : String(v);\n const fmtF = display: flex; align-items: center; justify-content: space-between;
(v, d=3) => v == null ? '—' : v.toFixed(d);\n\n function colorClass(id, val) margin-bottom: 1.25rem;
{\n const el = document.getElementById(id);\n el.classList.remove('positive','negative','neutral','loading');\n }
\ if (val > 0) el.classList.add('positive');\n else if (val < 0) el.classList.add('negative');\n .poly-title {
\ else el.classList.add('neutral');\n }\n\n async function fetchMetrics() display: flex; align-items: center; gap: 0.5rem;
{\n try {\n const r = await fetch(API, { cache: 'no-store' });\n if font-size: 0.95rem; font-weight: 600;
(!r.ok) throw new Error(r.status);\n const d = await r.json();\n\n document.getElementById('m-pnl').textContent }
\ = fmt$(d.total_pnl);\n document.getElementById('m-winrate').textContent .poly-badge {
\ = fmtPct(d.win_rate);\n document.getElementById('m-deployed').textContent font-size: 0.65rem; font-weight: 600; letter-spacing: 0.04em;
\ = fmt$(d.total_deployed);\n document.getElementById('m-trades').textContent padding: 2px 7px; border-radius: 20px;
\ = fmtN(d.total_trades);\n document.getElementById('m-bankroll').textContent background: rgba(188,140,255,0.15); color: var(--purple);
\ = fmt$(d.paper_bankroll);\n document.getElementById('m-sharpe').textContent border: 1px solid rgba(188,140,255,0.3);
\ = fmtF(d.sharpe_ratio, 2);\n document.getElementById('m-calibration').textContent }
= fmtF(d.calibration_score, 3);\n document.getElementById('m-status').textContent #poly-updated {
\ = d.promotion_ready ? '\U0001F680 Ready' : '⏳ Training';\n\n colorClass('m-pnl', font-size: 0.72rem; color: var(--muted);
d.total_pnl);\n colorClass('m-sharpe', d.sharpe_ratio);\n\n const }
mode = d.paper_mode ? 'PAPER' : 'LIVE';\n const badge = document.getElementById('poly-mode');\n
\ badge.textContent = mode;\n badge.style.background = d.paper_mode\n .metrics-grid {
\ ? 'rgba(188,140,255,0.15)' : 'rgba(63,185,80,0.15)';\n badge.style.color display: grid;
= d.paper_mode ? 'var(--purple)' : 'var(--green)';\n badge.style.borderColor grid-template-columns: repeat(2, 1fr);
= d.paper_mode\n ? 'rgba(188,140,255,0.3)' : 'rgba(63,185,80,0.3)';\n\n gap: 0.75rem;
\ const now = new Date();\n document.getElementById('poly-updated').textContent }
=\n 'Updated ' + now.toLocaleTimeString();\n } catch (e) {\n document.getElementById('poly-updated').textContent @media (min-width: 480px) {
= 'Error: ' + e.message;\n }\n }\n\n fetchMetrics();\n setInterval(fetchMetrics, .metrics-grid { grid-template-columns: repeat(4, 1fr); }
30000);\n </script>\n</body>\n</html>\n" }
.metric {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.875rem 1rem;
}
.metric-label {
font-size: 0.68rem; color: var(--muted);
text-transform: uppercase; letter-spacing: 0.06em;
margin-bottom: 0.35rem;
}
.metric-value {
font-size: 1.35rem; font-weight: 700; font-variant-numeric: tabular-nums;
transition: color 0.3s;
}
.metric-value.positive { color: var(--green); }
.metric-value.negative { color: var(--red); }
.metric-value.neutral { color: var(--text); }
.metric-value.loading { color: var(--muted); font-size: 1rem; }
/* ── Footer ──────────────────────────────────── */
footer {
color: var(--muted); font-size: 0.75rem; text-align: center;
width: 100%; max-width: 720px;
border-top: 1px solid var(--border);
padding-top: 1.25rem;
}
footer a { color: var(--accent); text-decoration: none; }
footer a:hover { text-decoration: underline; }
</style>
</head>
<body>
<header>
<div class="avatar">C</div>
<h1>ChemaVX</h1>
<p class="tagline">
<span class="dot"></span>Homelab · k3s · Self-hosted
</p>
<div class="badges">
<span class="badge"><b>k3s</b> · 2 nodos</span>
<span class="badge"><b>GitOps</b> · ArgoCD</span>
<span class="badge"><b>Secrets</b> · Infisical</span>
<span class="badge"><b>Backups</b> · Mega + B2</span>
<span class="badge"><b>Observabilidad</b> · Grafana + Kuma</span>
</div>
</header>
<!-- Services -->
<div style="width:100%;max-width:720px;margin-bottom:2.5rem;">
<p class="section-title">Services</p>
<div class="services-grid">
<a class="service-card" href="https://grafana.chemavx.xyz" target="_blank" rel="noopener">
<span class="service-icon">📊</span>
<span><span class="service-name">Grafana</span><div class="service-sub">Dashboards y alertas</div></span>
</a>
<a class="service-card" href="https://status.chemavx.xyz" target="_blank" rel="noopener">
<span class="service-icon">💓</span>
<span><span class="service-name">Uptime Kuma</span><div class="service-sub">Estado de servicios</div></span>
</a>
<a class="service-card" href="https://n8n.chemavx.xyz" target="_blank" rel="noopener">
<span class="service-icon">⚙️</span>
<span><span class="service-name">n8n</span><div class="service-sub">Automatización</div></span>
</a>
<a class="service-card" href="https://home.chemavx.xyz" target="_blank" rel="noopener">
<span class="service-icon">🏠</span>
<span><span class="service-name">Homarr</span><div class="service-sub">Panel del homelab</div></span>
</a>
<a class="service-card" href="https://git.chemavx.xyz" target="_blank" rel="noopener">
<span class="service-icon">🐙</span>
<span><span class="service-name">Gitea</span><div class="service-sub">Git + CI + registry</div></span>
</a>
<a class="service-card" href="https://argocd.chemavx.xyz" target="_blank" rel="noopener">
<span class="service-icon">🔄</span>
<span><span class="service-name">ArgoCD</span><div class="service-sub">GitOps del cluster</div></span>
</a>
<a class="service-card" href="https://infisical.chemavx.xyz" target="_blank" rel="noopener">
<span class="service-icon">🗝️</span>
<span><span class="service-name">Infisical</span><div class="service-sub">Gestor de secretos</div></span>
</a>
<a class="service-card" href="https://vaultwarden.chemavx.xyz" target="_blank" rel="noopener">
<span class="service-icon">🔐</span>
<span><span class="service-name">Vaultwarden</span><div class="service-sub">Contraseñas</div></span>
</a>
<a class="service-card" href="https://trilium.chemavx.xyz" target="_blank" rel="noopener">
<span class="service-icon">📝</span>
<span><span class="service-name">Trilium</span><div class="service-sub">Notas</div></span>
</a>
<a class="service-card" href="https://files.chemavx.xyz" target="_blank" rel="noopener">
<span class="service-icon">📁</span>
<span><span class="service-name">Files</span><div class="service-sub">Filebrowser</div></span>
</a>
<a class="service-card" href="https://umami.chemavx.xyz" target="_blank" rel="noopener">
<span class="service-icon">📈</span>
<span><span class="service-name">Umami</span><div class="service-sub">Analytics sin cookies</div></span>
</a>
<a class="service-card" href="https://ollama.chemavx.xyz" target="_blank" rel="noopener">
<span class="service-icon">🦙</span>
<span><span class="service-name">Ollama</span><div class="service-sub">LLM local</div></span>
</a>
</div>
</div>
<!-- Projects -->
<div style="width:100%;max-width:720px;margin-bottom:2.5rem;">
<p class="section-title">Projects</p>
<div class="services-grid">
<a class="service-card" href="https://theexclusionzone.com" target="_blank" rel="noopener">
<span class="service-icon">🛸</span>
<span><span class="service-name">The Exclusion Zone</span><div class="service-sub">UAP research · EN</div></span>
</a>
<a class="service-card" href="https://zonadeexclusion.com" target="_blank" rel="noopener">
<span class="service-icon">📡</span>
<span><span class="service-name">Zona de Exclusión</span><div class="service-sub">Investigación UAP · ES</div></span>
</a>
<a class="service-card" href="https://git.chemavx.xyz/chemavx/researchowl" target="_blank" rel="noopener">
<span class="service-icon">🦉</span>
<span><span class="service-name">ResearchOwl</span><div class="service-sub">Bot de investigación</div></span>
</a>
<a class="service-card" href="/ze/">
<span class="service-icon">⌨️</span>
<span><span class="service-name">ze</span><div class="service-sub">CLI · manual</div></span>
</a>
</div>
</div>
<!-- Polymarket bot: archived case study (final figures, 2026-07-06) -->
<div class="poly-wrapper">
<p class="section-title">Polymarket Bot — Archived · Case study</p>
<div class="poly-card">
<div class="poly-header">
<div class="poly-title">
<span>📉</span>
<span>Paper Trading — Final Results</span>
<span class="poly-badge">ARCHIVED</span>
</div>
<span id="poly-updated">Final snapshot · 2026-07-06</span>
</div>
<div class="metrics-grid">
<div class="metric">
<div class="metric-label">Realized P&amp;L</div>
<div class="metric-value positive">+$247.78</div>
</div>
<div class="metric">
<div class="metric-label">Paper Trades</div>
<div class="metric-value neutral">12</div>
</div>
<div class="metric">
<div class="metric-label">Evaluations</div>
<div class="metric-value neutral">239k</div>
</div>
<div class="metric">
<div class="metric-label">Days Observed</div>
<div class="metric-value neutral">81</div>
</div>
</div>
<p style="font-size:0.8rem;color:var(--muted);margin-top:1rem;line-height:1.5;">
Bot bayesiano de paper trading sobre mercados de predicción, retirado en
julio de 2026 tras el bloqueo regulatorio de Polymarket en España.
Motor de replay determinista, ~239k evaluaciones archivadas y
observabilidad completa en k3s.
<a href="https://git.chemavx.xyz/chemavx/polymarket-bot" target="_blank" rel="noopener"
style="color:var(--accent);text-decoration:none;">Case study completo →</a>
</p>
</div>
</div>
<footer>
<p>
Hosted on <strong>k3s</strong> &mdash;
<a href="https://grafana.chemavx.xyz/d/chemavx-homelab-v1/chemavx-homelab-overview" target="_blank">Cluster Dashboard</a>
&mdash; <a href="https://status.chemavx.xyz" target="_blank">Status</a>
&mdash; <a href="https://git.chemavx.xyz" target="_blank">Gitea</a>
</p>
</footer>
</body>
</html>
kind: ConfigMap kind: ConfigMap
metadata: metadata:
name: portfolio-html name: portfolio-html
+61
View File
@@ -0,0 +1,61 @@
# reddit-intel — promoción de docker compose (n97) a k3s
Estado: **INACTIVO**. Los manifiestos de este directorio existen solo en git;
el app-of-apps `k8s-manifests` no es recursivo, así que ArgoCD no los
sincroniza. La app corre en docker compose en chemavx-n97
(`/opt/reddit-intel`, semana de calibración desde 2026-07-06).
## Checklist de activación (en este orden)
1. **Imagen en el registro.** Primer build manual, o `workflow_dispatch` del
workflow "Build reddit-intel image" en el repo
`git.chemavx.xyz/chemavx/reddit-intel` (requiere secrets `CI_TOKEN` y
`TELEGRAM_BOT_TOKEN` configurados en ese repo). El workflow actualiza el
tag de `deployment.yaml` aquí; si el build es manual, sustituir el
placeholder `:00000000` a mano por el sha8 real.
2. **Secrets en Infisical** (proyecto "homelab", env `prod`, path
`/reddit-intel`) — mismos valores que el `.env` del compose:
`reddit-client-id`, `reddit-client-secret`, `llm-api-key`,
`dashboard-auth-token`, `telegram-bot-token`, `telegram-chat-id`
(los dos de telegram pueden ir vacíos).
3. **Congelar el compose y copiar la BD al PVC:**
```bash
# en n97
cd /opt/reddit-intel && docker compose down # para la app (checkpoint WAL limpio)
/opt/reddit-intel/scripts/backup.sh # backup final pre-migración
# activar la app (crea PVCs y arranca el pod con BD vacía)
kubectl apply -f reddit-intel/argocd-app.yaml
kubectl -n reddit-intel rollout status deploy/reddit-intel
# parar el pod y volcar la BD del host al PVC
kubectl -n reddit-intel scale deploy/reddit-intel --replicas=0
kubectl -n reddit-intel run pvc-loader --image=busybox --restart=Never \
--overrides='{"spec":{"nodeSelector":{"kubernetes.io/hostname":"chemavx-n97"},"containers":[{"name":"pvc-loader","image":"busybox","command":["sleep","600"],"volumeMounts":[{"name":"data","mountPath":"/data"}]}],"volumes":[{"name":"data","persistentVolumeClaim":{"claimName":"reddit-intel-data"}}]}}'
kubectl -n reddit-intel wait --for=condition=Ready pod/pvc-loader
kubectl -n reddit-intel cp /opt/reddit-intel/data/reddit-intel.db pvc-loader:/data/reddit-intel.db
kubectl -n reddit-intel delete pod pvc-loader
kubectl -n reddit-intel scale deploy/reddit-intel --replicas=1
```
(El `docker compose down` previo garantiza que no hay `-wal`/`-shm`
pendientes: basta copiar el `.db`.)
4. **Verificar:** `kubectl -n reddit-intel logs deploy/reddit-intel` debe
mostrar `scheduler_start jobs=monitor,detector,analyzer,karma,learning`;
el dashboard responde vía `svc/reddit-intel` (port-forward o el ingress
que se decida — pendiente de elegir exposición).
5. **Desmantelar la fase compose:** quitar la línea del crontab de chemavx en
n97 (el CronJob `reddit-intel-db-backup` de aquí la reemplaza) y archivar
`/opt/reddit-intel` cuando la semana de convivencia confirme que todo va bien.
## Notas
- El CronJob de backup (`backup-cronjob.yaml`) replica el patrón researchowl:
`sqlite3 .backup` + `integrity_check` + retención 7, contra el PVC.
- Un solo replica siempre (`strategy: Recreate`): SQLite WAL + APScheduler
in-process no admiten dos pods.
- El namespace `reddit-intel` ya existe en el clúster (creado durante la
preparación); `CreateNamespace=true` lo cubre igualmente.
+26
View File
@@ -0,0 +1,26 @@
# INACTIVO hasta la promoción: el app-of-apps k8s-manifests NO es recursivo,
# así que este fichero solo existe en git — nadie lo aplica automáticamente.
# La activación es manual: ver PROMOTION.md en este directorio.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: reddit-intel
namespace: argocd
annotations:
notifications.argoproj.io/subscribe.on-sync-failed.telegram: "5138407666"
notifications.argoproj.io/subscribe.on-health-degraded.telegram: "5138407666"
spec:
project: default
source:
repoURL: https://git.chemavx.xyz/chemavx/k8s-manifests
targetRevision: HEAD
path: reddit-intel
destination:
server: https://kubernetes.default.svc
namespace: reddit-intel
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
+80
View File
@@ -0,0 +1,80 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
annotations:
# Never let ArgoCD prune this PVC: it holds the DB backups.
argocd.argoproj.io/sync-options: Prune=false
name: reddit-intel-backups
namespace: reddit-intel
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: reddit-intel-db-backup
namespace: reddit-intel
labels:
app: reddit-intel
spec:
schedule: "0 3 * * *"
timeZone: "Europe/Madrid"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 600
template:
metadata:
labels:
app: reddit-intel-db-backup
spec:
restartPolicy: Never
containers:
- name: backup
image: keinos/sqlite3:latest
command:
- /bin/sh
- -c
- |
set -eu
STAMP=$(date +%Y%m%d)
DEST="/backups/reddit-intel-${STAMP}.db"
echo "Backing up /data/reddit-intel.db -> ${DEST}"
sqlite3 -cmd ".timeout 30000" /data/reddit-intel.db ".backup '${DEST}'"
RESULT=$(sqlite3 "${DEST}" "PRAGMA integrity_check;")
if [ "${RESULT}" != "ok" ]; then
echo "Integrity check FAILED: ${RESULT}"
rm -f "${DEST}"
exit 1
fi
echo "Integrity check ok"
# Retention: keep the 7 most recent backups
for f in $(ls -1t /backups/reddit-intel-*.db | tail -n +8); do
echo "Pruning old backup ${f}"
rm -f "${f}"
done
ls -lh /backups/
volumeMounts:
# NOT readOnly: the source DB runs in WAL mode and sqlite readers
# must be able to touch the -shm/-wal sidecar files. The job only
# ever reads the DB (.backup); it never modifies application data.
- name: data
mountPath: /data
- name: backups
mountPath: /backups
volumes:
- name: data
persistentVolumeClaim:
claimName: reddit-intel-data
- name: backups
persistentVolumeClaim:
claimName: reddit-intel-backups
+124
View File
@@ -0,0 +1,124 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: reddit-intel
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
annotations:
# Never let ArgoCD prune this PVC: it holds the SQLite DB.
argocd.argoproj.io/sync-options: Prune=false
name: reddit-intel-data
namespace: reddit-intel
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: reddit-intel
namespace: reddit-intel
labels:
app: reddit-intel
spec:
replicas: 1
# Single-replica app on a RWO local-path SQLite DB (WAL) with an in-process
# APScheduler: Recreate avoids two pods briefly sharing /data/reddit-intel.db.
strategy:
type: Recreate
selector:
matchLabels:
app: reddit-intel
template:
metadata:
labels:
app: reddit-intel
spec:
containers:
- name: reddit-intel
# Placeholder tag: el workflow manual (repo reddit-intel) lo sustituye
# por el sha8 real en cada build. Ver PROMOTION.md.
image: git.chemavx.xyz/chemavx/reddit-intel:00000000
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: DB_PATH
value: "/data/reddit-intel.db"
- name: REDDIT_USERNAME
value: "ExclusionZoneUAP"
- name: REDDIT_USER_AGENT
value: "linux:reddit-intel:v1.0 (by /u/ExclusionZoneUAP)"
- name: LLM_PROVIDER
value: "anthropic"
- name: LLM_MODEL_SCORING
value: "claude-haiku-4-5"
- name: LLM_MODEL_DRAFTING
value: "claude-sonnet-5"
- name: DAILY_LLM_BUDGET
value: "60"
- name: REDDIT_CLIENT_ID
valueFrom:
secretKeyRef:
name: reddit-intel-secrets
key: reddit-client-id
- name: REDDIT_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: reddit-intel-secrets
key: reddit-client-secret
- name: LLM_API_KEY
valueFrom:
secretKeyRef:
name: reddit-intel-secrets
key: llm-api-key
- name: DASHBOARD_AUTH_TOKEN
valueFrom:
secretKeyRef:
name: reddit-intel-secrets
key: dashboard-auth-token
- name: TELEGRAM_BOT_TOKEN
valueFrom:
secretKeyRef:
name: reddit-intel-secrets
key: telegram-bot-token
- name: TELEGRAM_CHAT_ID
valueFrom:
secretKeyRef:
name: reddit-intel-secrets
key: telegram-chat-id
volumeMounts:
- name: data
mountPath: /data
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 20
periodSeconds: 60
resources:
requests:
memory: "128Mi"
cpu: "50m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: data
persistentVolumeClaim:
claimName: reddit-intel-data
imagePullSecrets:
- name: gitea-registry
@@ -2,7 +2,7 @@ apiVersion: secrets.infisical.com/v1beta1
kind: InfisicalStaticSecret kind: InfisicalStaticSecret
metadata: metadata:
name: gitea-registry name: gitea-registry
namespace: polymarket-bot namespace: reddit-intel
spec: spec:
# References the SHARED auth layer in infisical-operator (cross-namespace). # References the SHARED auth layer in infisical-operator (cross-namespace).
infisicalAuthRef: infisicalAuthRef:
@@ -11,13 +11,13 @@ spec:
sources: sources:
- projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab" - projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab"
environmentSlug: prod environmentSlug: prod
secretPath: /registry # shared /registry v3 fields (same as researchowl) secretPath: /registry
syncOptions: syncOptions:
refreshInterval: 60s refreshInterval: 60s
targets: targets:
- kind: Secret - kind: Secret
name: gitea-registry-infisical # parallel name — old gitea-registry stays live until cutover name: gitea-registry
namespace: polymarket-bot namespace: reddit-intel
creationPolicy: Owner creationPolicy: Owner
secretType: kubernetes.io/dockerconfigjson secretType: kubernetes.io/dockerconfigjson
template: template:
@@ -0,0 +1,26 @@
apiVersion: secrets.infisical.com/v1beta1
kind: InfisicalStaticSecret
metadata:
name: reddit-intel-secrets
namespace: reddit-intel
spec:
# Shared, read-only machine identity (no write grant, no PushSecret).
# Source is the /reddit-intel folder. Expected keys (lowercase-hyphenated,
# straight passthrough to the deployment's secretKeyRef values):
# reddit-client-id, reddit-client-secret, llm-api-key,
# dashboard-auth-token, telegram-bot-token, telegram-chat-id
# telegram-* pueden crearse vacíos (las notificaciones son opcionales).
infisicalAuthRef:
name: infisical-auth
namespace: infisical-operator
sources:
- projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab"
environmentSlug: prod
secretPath: /reddit-intel
syncOptions:
refreshInterval: 60s
targets:
- kind: Secret
name: reddit-intel-secrets
namespace: reddit-intel
creationPolicy: Owner
+14
View File
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: reddit-intel
namespace: reddit-intel
labels:
app: reddit-intel
spec:
selector:
app: reddit-intel
ports:
- name: http
port: 80
targetPort: 8080
+2 -2
View File
@@ -44,7 +44,7 @@ spec:
spec: spec:
containers: containers:
- name: researchowl - name: researchowl
image: git.chemavx.xyz/chemavx/researchowl:d31badeb image: git.chemavx.xyz/chemavx/researchowl:dadc030d
imagePullPolicy: Always imagePullPolicy: Always
env: env:
- name: TELEGRAM_BOT_TOKEN - name: TELEGRAM_BOT_TOKEN
@@ -104,7 +104,7 @@ spec:
memory: "256Mi" memory: "256Mi"
cpu: "100m" cpu: "100m"
limits: limits:
memory: "1Gi" memory: "2Gi"
cpu: "500m" cpu: "500m"
volumes: volumes:
- name: data - name: data
@@ -1,20 +1,22 @@
# Se aplica una vez imperativamente (kubectl apply -f); después ArgoCD
# se autogestiona porque este fichero también vive en k8s-manifests/roswell/.
apiVersion: argoproj.io/v1alpha1 apiVersion: argoproj.io/v1alpha1
kind: Application kind: Application
metadata: metadata:
name: openclaw name: roswell
namespace: argocd namespace: argocd
spec: spec:
destination:
namespace: openclaw
server: https://kubernetes.default.svc
project: default project: default
source: source:
path: openclaw
repoURL: https://git.chemavx.xyz/chemavx/k8s-manifests repoURL: https://git.chemavx.xyz/chemavx/k8s-manifests
targetRevision: main targetRevision: HEAD
path: roswell
destination:
server: https://kubernetes.default.svc
namespace: roswell
syncPolicy: syncPolicy:
automated: automated:
prune: true prune: true
selfHeal: true selfHeal: true
syncOptions: syncOptions:
- CreateNamespace=true - CreateNamespace=true
+78
View File
@@ -0,0 +1,78 @@
# Backup diario de la BD del corpus con retención 7 (PLAN §Fase 6).
# pg_dump en formato custom (-Fc, comprimido) verificado con pg_restore --list.
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
annotations:
# ArgoCD no debe podar este PVC: contiene los backups de la BD.
argocd.argoproj.io/sync-options: Prune=false
name: roswell-backups
namespace: roswell
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: roswell-db-backup
namespace: roswell
labels:
app: roswell
spec:
schedule: "30 3 * * *"
timeZone: "Europe/Madrid"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 900
template:
metadata:
labels:
app: roswell-db-backup
spec:
restartPolicy: Never
containers:
- name: backup
# Misma imagen que el postgres del corpus: pg_dump 16 garantizado
# y ya está en la caché del nodo.
image: pgvector/pgvector:pg16
env:
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: roswell-pg-secret-infisical
key: password
command:
- /bin/sh
- -c
- |
set -eu
STAMP=$(date +%Y%m%d)
DEST="/backups/roswell-${STAMP}.dump"
echo "pg_dump roswell -> ${DEST}"
pg_dump -h postgres.roswell.svc.cluster.local -U roswell \
-d roswell -Fc -f "${DEST}"
# Verificación: un dump corrupto no lista su TOC.
pg_restore --list "${DEST}" > /dev/null
echo "Dump verificado ($(du -h ${DEST} | cut -f1))"
# Retención: conservar los 7 más recientes
for f in $(ls -1t /backups/roswell-*.dump | tail -n +8); do
echo "Podando backup antiguo ${f}"
rm -f "${f}"
done
ls -lh /backups/
volumeMounts:
- name: backups
mountPath: /backups
volumes:
- name: backups
persistentVolumeClaim:
claimName: roswell-backups
+116
View File
@@ -0,0 +1,116 @@
# Bot + scheduler en un pod (PLAN §Fase 6). Los secretos vienen de Infisical
# (carpeta /roswell del proyecto homelab) vía infisical-roswell-secrets.yaml.
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
annotations:
# ArgoCD no debe podar este PVC: guarda la caché del modelo Whisper (~3GB)
# y los PDFs subidos por Telegram.
argocd.argoproj.io/sync-options: Prune=false
name: roswell-data
namespace: roswell
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: roswell
namespace: roswell
labels:
app: roswell
spec:
replicas: 1
# Dos pods haciendo polling del mismo token de Telegram entran en conflicto:
# Recreate garantiza que el viejo muere antes de arrancar el nuevo.
strategy:
type: Recreate
selector:
matchLabels:
app: roswell
template:
metadata:
labels:
app: roswell
spec:
containers:
- name: roswell
# Tag placeholder: el workflow de Gitea Actions lo sustituye por el
# sha8 real en cada build (ver .gitea/workflows/build.yml del repo).
image: git.chemavx.xyz/chemavx/roswell-corpus:09457a47
imagePullPolicy: Always
env:
- name: TELEGRAM_BOT_TOKEN
valueFrom:
secretKeyRef:
name: roswell-secrets-infisical
key: telegram-bot-token
- name: TELEGRAM_ALLOWED_USERS
valueFrom:
secretKeyRef:
name: roswell-secrets-infisical
key: telegram-allowed-users
- name: ANTHROPIC_API_KEY
valueFrom:
secretKeyRef:
name: roswell-secrets-infisical
key: anthropic-api-key
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: roswell-secrets-infisical
key: database-url
- name: PODCASTINDEX_KEY
valueFrom:
secretKeyRef:
name: roswell-secrets-infisical
key: podcastindex-key
- name: PODCASTINDEX_SECRET
valueFrom:
secretKeyRef:
name: roswell-secrets-infisical
key: podcastindex-secret
- name: OLLAMA_URL
value: "http://ollama.ollama.svc.cluster.local:11434"
- name: SEARXNG_URL
value: "http://searxng-svc.researchowl.svc.cluster.local:8080"
- name: WHISPER_MODEL
value: "large-v3"
- name: WHISPER_DEVICE
value: "cpu"
- name: DATA_DIR
value: "/data"
- name: FEED_CHECK_HOURS
value: "24"
# Caché de modelos Whisper en el PVC: la descarga (~3GB) sobrevive
# a los redeploys.
- name: HF_HOME
value: "/data/hf-cache"
volumeMounts:
- name: data
mountPath: /data
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
# Whisper large-v3 INT8 en CPU: el pico real supera los 4Gi que
# estimaba el PLAN §10 (OOMKilled verificado el 2026-07-13).
# Sin límite de CPU: la transcripción usa los cores libres.
memory: "6Gi"
# Nodo grande (16 cores / 29Gi): transcribe a ~1x tiempo real y comparte
# nodo con los PVC de datos y backups. El nodo pequeño (4 cores/12Gi)
# se saturaba con Whisper.
nodeSelector:
kubernetes.io/hostname: chemavx-k8
volumes:
- name: data
persistentVolumeClaim:
claimName: roswell-data
imagePullSecrets:
- name: gitea-registry
+27
View File
@@ -0,0 +1,27 @@
apiVersion: secrets.infisical.com/v1beta1
kind: InfisicalStaticSecret
metadata:
name: gitea-registry
namespace: roswell
spec:
# Referencia la capa de auth COMPARTIDA en infisical-operator (cross-namespace).
infisicalAuthRef:
name: infisical-auth
namespace: infisical-operator
sources:
- projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab"
environmentSlug: prod
secretPath: /registry
syncOptions:
refreshInterval: 60s
targets:
- kind: Secret
name: gitea-registry
namespace: roswell
creationPolicy: Owner
secretType: kubernetes.io/dockerconfigjson
template:
engineVersion: v1
data:
# Reconstruye el pull-secret docker desde los dos campos de /registry.
.dockerconfigjson: '{"auths":{"git.chemavx.xyz":{"username":"{{ .GITEA_REGISTRY_USERNAME.Value }}","password":"{{ .GITEA_REGISTRY_TOKEN.Value }}","auth":"{{ printf "%s:%s" .GITEA_REGISTRY_USERNAME.Value .GITEA_REGISTRY_TOKEN.Value | encodeBase64 }}"}}}'
+69
View File
@@ -0,0 +1,69 @@
# Secretos de la app respaldados por Infisical (patrón researchowl).
#
# Claves esperadas en /roswell (proyecto homelab, entorno prod):
# TELEGRAM_BOT_TOKEN, TELEGRAM_ALLOWED_USERS, ANTHROPIC_API_KEY,
# PODCASTINDEX_KEY, PODCASTINDEX_SECRET, POSTGRES_PASSWORD
#
# Un InfisicalStaticSecret por target: el operator (versión actual) solo
# procesa el primer elemento de `targets`, comprobado el 2026-07-13.
#
# CORTE (cuando los valores estén verificados): cambiar las referencias de
# deployment.yaml, postgres.yaml y backup-cronjob.yaml a los nombres
# *-infisical y borrar los planos:
# kubectl delete secret roswell-secrets roswell-pg-secret -n roswell
apiVersion: secrets.infisical.com/v1beta1
kind: InfisicalStaticSecret
metadata:
name: roswell-secrets
namespace: roswell
spec:
# Identidad compartida de solo lectura (sin write, sin PushSecret).
infisicalAuthRef:
name: infisical-auth
namespace: infisical-operator
sources:
- projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab"
environmentSlug: prod
secretPath: /roswell
syncOptions:
refreshInterval: 60s
targets:
- kind: Secret
name: roswell-secrets-infisical
namespace: roswell
creationPolicy: Owner
template:
engineVersion: v1
data:
telegram-bot-token: '{{ .TELEGRAM_BOT_TOKEN.Value }}'
telegram-allowed-users: '{{ .TELEGRAM_ALLOWED_USERS.Value }}'
anthropic-api-key: '{{ .ANTHROPIC_API_KEY.Value }}'
podcastindex-key: '{{ .PODCASTINDEX_KEY.Value }}'
podcastindex-secret: '{{ .PODCASTINDEX_SECRET.Value }}'
# La URL completa se reconstruye: un solo sitio para la password.
database-url: 'postgresql://roswell:{{ .POSTGRES_PASSWORD.Value }}@postgres.roswell.svc.cluster.local:5432/roswell'
---
apiVersion: secrets.infisical.com/v1beta1
kind: InfisicalStaticSecret
metadata:
name: roswell-pg-secret
namespace: roswell
spec:
infisicalAuthRef:
name: infisical-auth
namespace: infisical-operator
sources:
- projectId: 17e98e9d-70f5-43d1-8382-7da818dfcdd0 # project "homelab"
environmentSlug: prod
secretPath: /roswell
syncOptions:
refreshInterval: 60s
targets:
- kind: Secret
name: roswell-pg-secret-infisical
namespace: roswell
creationPolicy: Owner
template:
engineVersion: v1
data:
password: '{{ .POSTGRES_PASSWORD.Value }}'
+90
View File
@@ -0,0 +1,90 @@
# Postgres 16 + pgvector para el corpus (namespace roswell).
# El Secret roswell-pg-secret se crea imperativamente (PLAN Fase 6);
# migración a Infisical preparada en infisical-roswell-secrets.yaml.
# ClusterIP (cierre Fase 6): con el bot ya en el clúster el NodePort 30432
# sobraba. Acceso local puntual: desde un nodo del clúster la ClusterIP
# responde directa; desde fuera, kubectl port-forward -n roswell svc/postgres 15432:5432.
apiVersion: v1
kind: Namespace
metadata:
name: roswell
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
annotations:
# ArgoCD no debe podar nunca este PVC: contiene la BD del corpus.
argocd.argoproj.io/sync-options: Prune=false
name: roswell-pgdata
namespace: roswell
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 20Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
namespace: roswell
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: roswell-postgres
template:
metadata:
labels:
app: roswell-postgres
spec:
containers:
- name: postgres
image: pgvector/pgvector:pg16
env:
- name: POSTGRES_DB
value: roswell
- name: POSTGRES_USER
value: roswell
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: roswell-pg-secret-infisical
key: password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
ports:
- containerPort: 5432
volumeMounts:
- name: pgdata
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 1Gi
readinessProbe:
exec:
command: ["pg_isready", "-U", "roswell", "-d", "roswell"]
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: pgdata
persistentVolumeClaim:
claimName: roswell-pgdata
---
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: roswell
spec:
type: ClusterIP
selector:
app: roswell-postgres
ports:
- port: 5432
targetPort: 5432