refactor(structure): reorganize monorepo with projects, terraform hierarchy, and strict k8s namespace rules
@@ -14,36 +14,32 @@ This repository is divided into distinct domains, each with its own `AGENTS.md`
|
||||
|
||||
### 2. [Kubernetes Workloads](./k8s/AGENTS.md) (`k8s/`)
|
||||
* **Purpose**: Deploying applications and services to the cluster.
|
||||
* **Method**: GitOps-style directory structure (Namespace = Folder).
|
||||
* **Constraints**: Enforces `ingress-nginx`, `cert-manager` (Let's Encrypt), and `git-crypt` for secrets.
|
||||
* **Strict Namespace Rule**: **Every top-level subdirectory in `k8s/` strictly corresponds to a Kubernetes namespace.** Non-namespace folders, loose scripts, or documentation do not belong here.
|
||||
* **Constraints**: Enforces `ingress-nginx` / `traefik`, `cert-manager` (Let's Encrypt), and `git-crypt` for secrets.
|
||||
* **OpenClaw**: For OpenClaw AI agent deployment and troubleshooting, see [k8s/openclaw/AGENTS.md](./k8s/openclaw/AGENTS.md) and [k8s/openclaw/TROUBLESHOOTING.md](./k8s/openclaw/TROUBLESHOOTING.md).
|
||||
* **Read**: [k8s/AGENTS.md](./k8s/AGENTS.md)
|
||||
|
||||
### 3. Terraform (`terraform/`)
|
||||
* **Purpose**: Infrastructure as Code for external resources, primarily **DNS**.
|
||||
* **Provider**: OVH (`ovh/ovh`).
|
||||
* **Scope**:
|
||||
* **DNS Zones**: `haumdaucher.de`, `moritzgraf.de`, `ameliegraf.de`, and others.
|
||||
* **Mail Config**: Google Workspace MX/SPF/DKIM records.
|
||||
* **Cluster IP**: Points domains to the Haumdaucher node IP (`136.243.23.215`).
|
||||
* **Usage**:
|
||||
```bash
|
||||
cd terraform
|
||||
terraform init
|
||||
terraform apply
|
||||
```
|
||||
* **Purpose**: Infrastructure as Code divided into two symmetrical modules:
|
||||
* **Cloud & DNS (`terraform/infrastructure/`)**:
|
||||
* **DNS Zones**: `haumdaucher.de`, `moritzgraf.de`, `ameliegraf.de`, and others via OVH (`ovh/ovh`).
|
||||
* **Cloud Services**: Authentik SSO (`authentik.tf`), GCP APIs, backups, and Google Workspace MX/SPF/DKIM.
|
||||
* **Usage**: `cd terraform/infrastructure && terraform apply`
|
||||
* **Smart Home Shelly Automation (`terraform/shelly/`)**:
|
||||
* **Taupi Cellar Mold Ventilation**: Automates Shelly Plug S with psychrometric dew point calculations (`Taupi-4.0.js`) and BTHome Bluetooth sensor telemetry.
|
||||
* **Usage**: `cd terraform/shelly && terraform apply`
|
||||
* **Read**: [terraform/shelly/AGENTS.md](./terraform/shelly/AGENTS.md)
|
||||
|
||||
### 4. Monitoring (`monitoring/`)
|
||||
* **Purpose**: IoT and physical device monitoring, separate from K8s metrics.
|
||||
* **Purpose**: Physical ambient BLE sensor fleet monitoring across living areas.
|
||||
* **Tool**: [ESPHome](https://esphome.io).
|
||||
* **Devices**: Temperature sensors, plant monitors (Xiaomi/BLE), ESP32 bridges.
|
||||
* **Devices**: Temperature & humidity sensors, Xiaomi BLE plants, ESP32 bridges (`muc-kitchen`, `muc-living`, `rgbg-living`).
|
||||
* **Usage**:
|
||||
```bash
|
||||
cd monitoring
|
||||
# Flash or update a device
|
||||
esphome run config/<device>.secret.yml
|
||||
```
|
||||
* **Network**: Devices typically operate on static IPs in the `192.168.x.x` range.
|
||||
* **Read**: [monitoring/README.md](./monitoring/README.md)
|
||||
|
||||
### 5. Knowledge Wiki & OpenClaw Tasks (`knowledge-wiki/`)
|
||||
* **Purpose**: Shared personal knowledge wiki and task dispatch between OpenClaw (mobile/Telegram concierge) and Antigravity (local pair programming agent).
|
||||
@@ -56,7 +52,7 @@ This repository is divided into distinct domains, each with its own `AGENTS.md`
|
||||
3. Implement the chosen task in the appropriate repository files.
|
||||
4. Move the task to `## ✅ Completed Tasks` with completion details and commit info.
|
||||
5. Push back: `git -C knowledge-wiki commit -am "chore(tasks): complete <task>" && git -C knowledge-wiki push origin main`.
|
||||
- When Antigravity needs knowledge about family, home, servers, or setup:
|
||||
|
||||
### 6. Architecture Decision Records (`docs/adr/`)
|
||||
* **Purpose**: Formal record of architectural decisions, technical trade-offs, and design standards across the monorepo.
|
||||
* **Format**: [MADR](https://adr.github.io/madr/) (Markdown Architectural Decision Records).
|
||||
@@ -75,7 +71,7 @@ This repository is divided into distinct domains, each with its own `AGENTS.md`
|
||||
* **Purpose**: Personal homepage and portfolio for Moritz Graf (`https://moritzgraf.de`).
|
||||
* **Manifest**: `k8s/web/moritzgrafde.yaml` (Namespace: `web`).
|
||||
* **Image**: `registry.haumdaucher.de/moritzgrafde:latest`.
|
||||
* **Key Rules**: Executive portfolio built with Vue 3, Vite, TypeScript, and Tailwind CSS served via Nginx in Docker. Migrated from legacy Hugo to a modern reactive platform showcase.
|
||||
* **Key Rules**: Executive portfolio built with Vue 3, Vite, TypeScript, and Tailwind CSS served via Nginx in Docker.
|
||||
* **Read**: [projects/moritzgrafde/AGENTS.md](./projects/moritzgrafde/AGENTS.md)
|
||||
|
||||
### 9. [Haumdaucher Official Website](./projects/haumdaucherde/AGENTS.md) (`projects/haumdaucherde/`)
|
||||
@@ -85,6 +81,29 @@ This repository is divided into distinct domains, each with its own `AGENTS.md`
|
||||
* **Key Rules**: PWA built with Vue 3, Vite, TypeScript, and Firebase Auth; dynamic theming with NAT mode easter egg; strict compliance with German Vereinsrecht (§ 5 DDG Impressum for n.e.V.); user creation and mail forwarding Apps Script automations.
|
||||
* **Read**: [projects/haumdaucherde/AGENTS.md](./projects/haumdaucherde/AGENTS.md)
|
||||
|
||||
### 10. [Grabengasse 7 Home Portal](./projects/grabengasse/AGENTS.md) (`projects/grabengasse/`)
|
||||
* **Purpose**: Local climate, weather, and environmental dashboard for Grabengasse 7 (`https://grabengasse7.de`).
|
||||
* **Manifests**: `k8s/grabengasse/` (`frontend.yaml`, `backend.yaml`, `ingress.yaml` in namespace `grabengasse`).
|
||||
* **Images**: `registry.haumdaucher.de/website/grabengasse_frontend:latest`, `registry.haumdaucher.de/website/grabengasse_backend:latest`.
|
||||
* **Key Rules**: Vue 3 retro pixel art frontend + FastAPI backend querying cluster Prometheus for Shelly WS90 weather telemetry.
|
||||
* **Read**: [projects/grabengasse/AGENTS.md](./projects/grabengasse/AGENTS.md)
|
||||
|
||||
### 11. [Kitchen e-Paper Display](./projects/esphome_display_kitchen/AGENTS.md) (`projects/esphome_display_kitchen/`)
|
||||
* **Purpose**: Dedicated ESP32 microcontroller driving the Waveshare e-Paper status display in the kitchen.
|
||||
* **Integrations**: Home Assistant Native API (`input_text.epaper_message`), Prometheus client, SNTP time sync.
|
||||
* **Key Tools**: Interactive `uv run flash.py` wizard and `./send_message.sh` CLI push messaging.
|
||||
* **Read**: [projects/esphome_display_kitchen/AGENTS.md](./projects/esphome_display_kitchen/AGENTS.md)
|
||||
|
||||
### 12. Utility Scripts (`scripts/`)
|
||||
* **Purpose**: Cross-cutting operational and administration CLI scripts that do not belong inside Kubernetes namespace directories.
|
||||
* **Key Tools**:
|
||||
* `scripts/htpasswd.py`: Generates bcrypt-hashed htpasswd credentials for ingress basic auth secrets.
|
||||
|
||||
### 13. Network & Router Integration (`fritzbox/`)
|
||||
* **Purpose**: Site-to-Site WireGuard LAN coupling and DynDNS OVH synchronization for the home FritzBox router.
|
||||
* **Cluster Endpoint**: `k8s/wireguard/` (`vpn.haumdaucher.de`).
|
||||
* **Read**: [fritzbox/README.md](./fritzbox/README.md)
|
||||
|
||||
## Global Constraints
|
||||
* **Secrets**: `git-crypt` is used extensively. Ensure you have the unlock key before editing `*.secret` files.
|
||||
* **Identity**:
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
# Infrapuzzle
|
||||
# Infrapuzzle 🧩
|
||||
|
||||
Infrapuzzle is the newly restructured way of implementing my private infrastructure. Currently the following things are in project scope:
|
||||
Infrapuzzle is the monorepo for personal infrastructure, Kubernetes cluster operations, DNS, IoT monitoring, and hosted applications.
|
||||
|
||||
* single node k8s "cluster" implemented with kubespray
|
||||
* development with Vagrant
|
||||
* host directory storage
|
||||
* Ingress via host network
|
||||
## Repository Layout
|
||||
|
||||
## terraform
|
||||
- **[`bootstrap/`](bootstrap/)**: Provisioning the single-node Kubernetes cluster (`haumdaucher`) using Kubespray.
|
||||
- **[`k8s/`](k8s/)**: Kubernetes workloads. **Every subdirectory represents a Kubernetes namespace.**
|
||||
- **[`terraform/`](terraform/)**: Declarative Infrastructure as Code:
|
||||
- `terraform/infrastructure/`: Cloud infrastructure, OVH DNS zones, Authentik SSO, and Google Workspace.
|
||||
- `terraform/shelly/`: Smart home Shelly Plug S ventilation controller ("Taupi") for cellar mold prevention.
|
||||
- **[`projects/`](projects/)**: Self-contained hosted applications and projects:
|
||||
- `projects/sasch40/`: Retro birthday adventure game (`sasch40.haumdaucher.de`).
|
||||
- `projects/moritzgrafde/`: Personal homepage and executive portfolio (`moritzgraf.de`).
|
||||
- `projects/haumdaucherde/`: Official community website & portal (`haumdaucher.de`).
|
||||
- `projects/grabengasse/`: Climate & weather dashboard (`grabengasse7.de`).
|
||||
- `projects/esphome_display_kitchen/`: ESP32 e-Paper kitchen status display & flashing wizard.
|
||||
- **[`monitoring/`](monitoring/)**: ESPHome fleet for BLE ambient room temperature & humidity sensors.
|
||||
- **[`fritzbox/`](fritzbox/)**: Home router Site-to-Site WireGuard VPN coupling and DynDNS synchronization.
|
||||
- **[`scripts/`](scripts/)**: Operational utility scripts (e.g. `htpasswd.py`).
|
||||
- **[`knowledge-wiki/`](knowledge-wiki/)**: Shared personal knowledge base and OpenClaw task queue submodule.
|
||||
- **[`docs/`](docs/)**: Architecture Decision Records (ADRs) following the MADR standard.
|
||||
|
||||
[Documentation in subfolder](./terraform/README.md). It takes care of setting DNS records on OVH nameservers.
|
||||
|
||||
## bootstrap
|
||||
|
||||
[Documentation in subfolder](./bootstrap/README.md). It takes care of setting up the k8s cluster itself, including a vagrant dev environment.
|
||||
|
||||
## k8s
|
||||
|
||||
[Documentation in subfolder](./k8s/README.md). The services themselfes.
|
||||
|
||||
|
||||
|
||||
## Links used
|
||||
|
||||
* [ingress via host network](https://kubernetes.github.io/ingress-nginx/deploy/baremetal/#via-the-host-network)
|
||||
* [Install K8s with Kubespray](https://kubernetes.io/docs/setup/production-environment/tools/kubespray/)
|
||||
For architectural standards and operational guidelines, refer to [AGENTS.md](AGENTS.md).
|
||||
@@ -1,20 +0,0 @@
|
||||
# AGENTS.md (ESPHome Workspace)
|
||||
|
||||
This directory is a self-contained ESPHome project space utilizing dynamic configuration loaders and an interactive flashing CLI.
|
||||
|
||||
## Rules & Design Standards
|
||||
|
||||
### Secret Management
|
||||
* **NEVER** write plaintext Wi-Fi passwords, tokens, or API keys directly in the YAML configurations.
|
||||
* **DO NOT** modify the `.gitignore` to allow checking in `.env` files.
|
||||
* Use ESPHome's `!env_var VAR_NAME` to inject environment variables at compile-time. Any new secrets must be declared in `.env.template` and configured locally in `.env`.
|
||||
|
||||
### Configuration Standards
|
||||
* Default board architecture is ESP32 (`nodemcu-32s` with `arduino` framework) unless specified.
|
||||
* Naming conventions for configurations: `esdisplay_<name>.yaml` or `sensor_<name>.yaml`.
|
||||
* All displays should use the `time` component with SNTP sync for accurate time rendering.
|
||||
* Prefer importing sensors from Home Assistant over Native API client integrations, as it decouples rendering layouts from automation scripts.
|
||||
|
||||
### Build and Run Tools
|
||||
* Use `uv run flash.py` to compile, validate, clean, or flash ESPHome configurations.
|
||||
* Avoid triggering direct `esphome` cli calls unless doing isolated validation tests.
|
||||
@@ -11,8 +11,9 @@ This directory contains the Kubernetes manifests and Helm charts for a single-no
|
||||
* **SSL**: `cert-manager` (LetsEncrypt)
|
||||
|
||||
## Directory Structure
|
||||
* **Top-level folders**: Each folder corresponds to a Kubernetes **namespace**.
|
||||
* Example: `mailu/` contains resources for the `mailu` namespace.
|
||||
* **Strict Namespace Mapping**: **Every top-level subdirectory in `k8s/` MUST correspond strictly to a Kubernetes namespace.**
|
||||
* Example: `mailu/` contains resources for the `mailu` namespace, `grabengasse/` for `grabengasse`, `haumdaucher/` for `haumdaucher`, etc.
|
||||
* **NO non-namespace folders**: Loose utility scripts belong in the root `scripts/` directory, application source code belongs in `projects/`, and general documentation belongs in namespace READMEs or `docs/`.
|
||||
* **Documentation**: `README.md` is the **authoritative source** for deployment commands. Always check it before running commands.
|
||||
|
||||
## Code Style & Conventions
|
||||
|
||||
@@ -678,7 +678,7 @@ kubectl create ns migrate
|
||||
# backup on moritzgraf.de
|
||||
ssh moritzgraf.de "sudo su - docker -c 'cd /home/docker/mailu && docker-compose stop' && sudo su - root -c 'cd /home/docker && tar czvf mailu.tar.gz ./mailu && mv mailu.tar.gz /home/moritz/ && chown moritz. /home/moritz/mailu.tar.gz' && scp mailu.tar.gz haumdaucher.de:/home/moritz/"
|
||||
# terraform change
|
||||
cd ../terraform && terraform apply
|
||||
cd ../terraform/infrastructure && terraform apply
|
||||
# helm apply
|
||||
cd ../k8s
|
||||
helm upgrade --create-namespace --install mailu mailu/mailu -n mailu -f mailu/mailu.secret.yml
|
||||
@@ -873,13 +873,13 @@ Grabengasse 7 home website and backend service deployed in the `grabengasse` nam
|
||||
# Login to local registry (if not already logged in)
|
||||
docker login registry.haumdaucher.de -u moritz -p "PPf10musSh!!"
|
||||
|
||||
# Build and push backend image
|
||||
cd website/grabengasse_backend
|
||||
make all
|
||||
# Build and push images via root projects/grabengasse Makefile
|
||||
cd projects/grabengasse
|
||||
make build push
|
||||
|
||||
# Build and push frontend image
|
||||
cd ../grabengasse_frontend
|
||||
make all
|
||||
# Or build individual components:
|
||||
cd projects/grabengasse/frontend && make all
|
||||
cd ../backend && make all
|
||||
```
|
||||
|
||||
### Deploy to Kubernetes
|
||||
|
||||
@@ -170,10 +170,10 @@ The Authentik login screen is customized with a **Haumdaucher** glassmorphism th
|
||||
* **Branded Footer**: "HAUMDAUCHER Wurst und Spezialitäten n.e.V." subtle watermark.
|
||||
|
||||
### Managing Theme via Terraform
|
||||
Branding is provisioned via the `authentik_brand` resource in `terraform/authentik.tf` using the stylesheet `terraform/haumdaucher-theme.css`:
|
||||
Branding is provisioned via the `authentik_brand` resource in `terraform/infrastructure/authentik.tf` using the stylesheet `terraform/infrastructure/haumdaucher-theme.css`:
|
||||
|
||||
```bash
|
||||
cd terraform
|
||||
cd terraform/infrastructure
|
||||
terraform apply -target=authentik_brand.haumdaucher
|
||||
```
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ This guide covers everything you need to know about managing users in Authentik
|
||||
| Question | Answer & Key Mechanism |
|
||||
| :--- | :--- |
|
||||
| **How to add new users?** | **4 Ways**: (1) Admin Web UI (`Directory > Users`), (2) **Invitations flow** (`Directory > Invitations` - recommended for self-service), (3) **Terraform** (`authentik_user` resource), or (4) **REST API / Python script** for bulk imports. |
|
||||
| **Can this be done in Terraform?** | **Yes.** Use `resource "authentik_user"` in `terraform/authentik.tf`. Supports custom attributes via `jsonencode()`, group assignments, and active status. |
|
||||
| **Can this be done in Terraform?** | **Yes.** Use `resource "authentik_user"` in `terraform/infrastructure/authentik.tf`. Supports custom attributes via `jsonencode()`, group assignments, and active status. |
|
||||
| **Can I change the email sent to the user?** | **Yes.** Configured in **Flows & Stages > Stages > Email Stage**. You can customize the subject line, token expiration, and HTML template (written in Django template syntax with access to user fields and custom attributes). |
|
||||
| **Can I limit what a user has access to?** | **Yes.** Through **Policy Bindings** attached to Applications: Group Membership Policies (e.g., `infra-admins` vs `users`), IP/Geo Policies, or Python Expression Policies checking custom attributes (e.g., only active club members). |
|
||||
| **How to handle Excel tables with custom attributes?** | Authentik stores arbitrary key-value pairs in `user.attributes` (JSON). You can import an Excel/CSV file with a Python script or Terraform `csvdecode()`, map fields like `haumdaucher_mitgliedsnummer` or `second_email`, and expose them to downstream apps (Nextcloud, Grafana, Gitea) via OIDC Scope Mappings. |
|
||||
@@ -75,7 +75,7 @@ If you encounter this screen when opening an invite link:
|
||||
|
||||
### Solution: The Haumdaucher Custom Invitation Flow (`haumdaucher-invitation-flow`)
|
||||
|
||||
We provision a dedicated, streamlined flow in `terraform/authentik.tf`:
|
||||
We provision a dedicated, streamlined flow in `terraform/infrastructure/authentik.tf`:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
@@ -122,7 +122,7 @@ graph LR
|
||||
|
||||
## 🏗️ Method 2: User Provisioning via Terraform
|
||||
|
||||
You can manage users, groups, and permissions directly in `terraform/authentik.tf`.
|
||||
You can manage users, groups, and permissions directly in `terraform/infrastructure/authentik.tf`.
|
||||
|
||||
### 1. Single User Definition
|
||||
```hcl
|
||||
@@ -161,7 +161,7 @@ max.mustermann,Max Mustermann,max@example.com,max.privat@gmail.com,HM-2024-042,f
|
||||
anna.schmidt,Anna Schmidt,anna@example.com,anna.work@company.de,HM-2024-043,true
|
||||
```
|
||||
|
||||
In `terraform/authentik.tf`:
|
||||
In `terraform/infrastructure/authentik.tf`:
|
||||
```hcl
|
||||
locals {
|
||||
csv_users = csvdecode(file("${path.module}/users.csv"))
|
||||
@@ -479,7 +479,7 @@ graph TD
|
||||
| **haumdaucher.de** | `haumdaucher.de` | ForwardAuth / OIDC | **`Admin`** ✅ | **`Member Access`** ✅ | ❌ *Denied* |
|
||||
|
||||
### Policy Bindings in Terraform
|
||||
All policy bindings are enforced directly in `terraform/authentik.tf`:
|
||||
All policy bindings are enforced directly in `terraform/infrastructure/authentik.tf`:
|
||||
|
||||
```hcl
|
||||
# 1. Admin-Only Tools (Restricted strictly to infra-admins)
|
||||
|
||||
@@ -57,7 +57,7 @@ The dashboard is exposed on your custom domain with TLS and basic authentication
|
||||
* **Username:** `admin`
|
||||
* **Password:** `moritz-z2m-2026`
|
||||
|
||||
*Note: You can change the basic auth password by running the `k8s/htpasswd.py` script on your machine to generate a new bcrypt hash, then updating the `users` value in `k8s/monitoring/zigbee2mqtt.secret.yaml`.*
|
||||
*Note: You can change the basic auth password by running the `scripts/htpasswd.py` script on your machine to generate a new bcrypt hash, then updating the `users` value in `k8s/monitoring/zigbee2mqtt.secret.yaml`.*
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Nightly multi-agent session maintenance and compaction script for OpenClaw.
|
||||
1. Pre-warms OS page cache for SQLite databases on spinning disks.
|
||||
2. Recovers any wedged or locked sessions via recover_sessions.py.
|
||||
3. Prunes abandoned isolated cron sessions (>48h old) to prevent token/state bloat.
|
||||
4. Tier A Hard-Reset: Resets koch, hebamme, and shopping topics to 0 tokens (clean archive).
|
||||
5. Tier B Smart-Compaction: Compacts active sessions exceeding token thresholds (5s pacing).
|
||||
6. Checkpoints SQLite WAL journals across all agent databases to disk.
|
||||
7. Optimizes SQLite query planner without blocking full VACUUM.
|
||||
"""
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
THRESHOLD_TOKENS = 20000
|
||||
THRESHOLD_TRANSCRIPT_BYTES = 300000
|
||||
THRESHOLD_EVENT_COUNT = 40
|
||||
CRON_MAX_AGE_HOURS = 48
|
||||
RPC_TIMEOUT_MS = 180000
|
||||
|
||||
# Tier A: Event-logging agents whose state is 100% externalized (DuckDB / JSONL / Groceries DB).
|
||||
# Nightly reset to 0 prevents cross-day hallucination and eliminates LLM compaction token costs.
|
||||
TIER_A_RESET_TARGETS = [
|
||||
("koch", "agent:koch:telegram:group:-1004326921510:topic:21"),
|
||||
("hebamme", "agent:hebamme:telegram:group:-1004326921510:topic:22"),
|
||||
("shopping", "agent:shopping:telegram:group:-1004326921510:topic:115"),
|
||||
]
|
||||
|
||||
def prewarm_page_cache():
|
||||
print("=== Pre-warming SQLite databases into OS page cache ===")
|
||||
t0 = time.time()
|
||||
try:
|
||||
subprocess.run(
|
||||
"cat /home/node/.openclaw/state/openclaw.sqlite /home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite > /dev/null 2>&1 || true",
|
||||
shell=True,
|
||||
timeout=90.0
|
||||
)
|
||||
print(f"Page cache pre-warmed in {time.time() - t0:.2f}s")
|
||||
except Exception as e:
|
||||
print(f"Page cache pre-warming warning: {e}")
|
||||
|
||||
def prune_stale_cron_sessions():
|
||||
print(f"=== Pruning isolated cron sessions older than {CRON_MAX_AGE_HOURS}h ===")
|
||||
cutoff_ms = int(time.time() * 1000) - (CRON_MAX_AGE_HOURS * 3600 * 1000)
|
||||
total_pruned = 0
|
||||
total_tokens_reclaimed = 0
|
||||
|
||||
for db in sorted(glob.glob("/home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite")):
|
||||
agent = db.split("/")[5]
|
||||
try:
|
||||
conn = sqlite3.connect(db, timeout=30.0)
|
||||
conn.execute("PRAGMA foreign_keys = ON;")
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type=\"table\"")
|
||||
all_tables = set(r[0] for r in cursor.fetchall())
|
||||
|
||||
cursor.execute("SELECT session_key, current_session_id, updated_at, entry_json FROM session_nodes WHERE session_key LIKE \"%:cron:%\"")
|
||||
stale = []
|
||||
for k, sid, up, ej in cursor.fetchall():
|
||||
if up and up < cutoff_ms:
|
||||
tok = 0
|
||||
if ej:
|
||||
try:
|
||||
tok = json.loads(ej).get("totalTokens", 0) or 0
|
||||
except Exception:
|
||||
pass
|
||||
stale.append((k, sid, tok))
|
||||
|
||||
if stale:
|
||||
tokens_reclaimed = sum(t[2] for t in stale)
|
||||
for k, sid, _ in stale:
|
||||
for t in ["trajectory_runtime_events", "transcript_events", "transcript_event_identities", "session_transcript_active_events", "transcript_rewrite_watermarks", "session_transcript_index_state", "session_transcript_fts", "session_conversations"]:
|
||||
if t in all_tables and sid:
|
||||
cursor.execute(f"DELETE FROM {t} WHERE session_id = ?", (sid,))
|
||||
for t in ["session_participants", "heartbeat_outcomes", "session_progress_cards", "board_widgets", "board_tabs"]:
|
||||
if t in all_tables:
|
||||
cursor.execute(f"DELETE FROM {t} WHERE session_key = ?", (k,))
|
||||
if "memory_entry_origins" in all_tables:
|
||||
cursor.execute("DELETE FROM memory_entry_origins WHERE session_key = ? OR session_id = ?", (k, sid or ""))
|
||||
if "session_windows" in all_tables:
|
||||
cursor.execute("DELETE FROM session_windows WHERE session_key = ? OR session_id = ?", (k, sid or ""))
|
||||
cursor.execute("DELETE FROM session_nodes WHERE session_key = ?", (k,))
|
||||
conn.commit()
|
||||
total_pruned += len(stale)
|
||||
total_tokens_reclaimed += tokens_reclaimed
|
||||
print(f" [{agent}] Pruned {len(stale)} dead cron sessions ({tokens_reclaimed} tokens reclaimed)")
|
||||
|
||||
# Unconditionally resolve any dangling foreign key references across all tables
|
||||
while True:
|
||||
cursor.execute("PRAGMA foreign_key_check;")
|
||||
violations = cursor.fetchall()
|
||||
if not violations:
|
||||
break
|
||||
for table, rowid, parent, fkid in violations:
|
||||
cursor.execute(f"DELETE FROM {table} WHERE rowid = ?", (rowid,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f" [{agent}] Pruning error: {e}")
|
||||
|
||||
# Clear resolved quarantine entries if all databases are healthy
|
||||
try:
|
||||
q_path = "/home/node/.openclaw/state/openclaw-quarantine.sqlite"
|
||||
if os.path.exists(q_path):
|
||||
q_conn = sqlite3.connect(q_path)
|
||||
q_cur = q_conn.cursor()
|
||||
q_cur.execute("DELETE FROM quarantined_databases")
|
||||
q_conn.commit()
|
||||
q_conn.close()
|
||||
except Exception as e:
|
||||
print(f"Quarantine cleanup error: {e}")
|
||||
|
||||
print(f"Cron pruning complete: {total_pruned} sessions pruned, {total_tokens_reclaimed} tokens reclaimed.")
|
||||
|
||||
def reset_tier_a_sessions():
|
||||
print("=== Tier A: Daily Hard-Reset to 0 (koch, hebamme, shopping) ===")
|
||||
for agent, key in TIER_A_RESET_TARGETS:
|
||||
db_path = f"/home/node/.openclaw/agents/{agent}/agent/openclaw-agent.sqlite"
|
||||
if not os.path.exists(db_path):
|
||||
continue
|
||||
try:
|
||||
con = sqlite3.connect(db_path, timeout=10.0)
|
||||
row = con.execute("SELECT current_session_id FROM session_nodes WHERE session_key=?", (key,)).fetchone()
|
||||
con.close()
|
||||
if not row or not row[0]:
|
||||
print(f" [Tier A] {agent} ({key}) already at 0 (no active session).")
|
||||
continue
|
||||
print(f" [Tier A] Resetting {agent} ({key})...")
|
||||
r = subprocess.run(
|
||||
["openclaw", "sessions", "delete", key, "--agent", agent, "--yes"],
|
||||
capture_output=True, text=True, timeout=60.0
|
||||
)
|
||||
if r.returncode == 0:
|
||||
print(f" ✓ Reset success: {r.stdout.strip()}")
|
||||
else:
|
||||
print(f" ✗ Reset error: {r.stderr.strip()}")
|
||||
except Exception as e:
|
||||
print(f" ✗ Exception resetting {agent}: {e}")
|
||||
time.sleep(1)
|
||||
|
||||
def get_session_metrics(agent, key, sid=None):
|
||||
db_path = f"/home/node/.openclaw/agents/{agent}/agent/openclaw-agent.sqlite"
|
||||
if not os.path.exists(db_path):
|
||||
return 0, 0
|
||||
try:
|
||||
con = sqlite3.connect(db_path, timeout=5.0)
|
||||
cur = con.cursor()
|
||||
if not sid:
|
||||
row = cur.execute("SELECT current_session_id FROM session_nodes WHERE session_key=?", (key,)).fetchone()
|
||||
sid = row[0] if row else None
|
||||
if not sid:
|
||||
con.close()
|
||||
return 0, 0
|
||||
ev_cnt, ev_bytes = cur.execute(
|
||||
"SELECT count(*), coalesce(sum(length(event_json)), 0) FROM transcript_events WHERE session_id=?",
|
||||
(sid,)
|
||||
).fetchone()
|
||||
con.close()
|
||||
return ev_cnt, ev_bytes
|
||||
except Exception:
|
||||
return 0, 0
|
||||
|
||||
def compact_active_sessions():
|
||||
tier_a_keys = set(t[1] for t in TIER_A_RESET_TARGETS)
|
||||
print(f"=== Tier B: Running Multi-Agent Compaction (threshold: {THRESHOLD_TOKENS} tokens / {THRESHOLD_TRANSCRIPT_BYTES//1024}KB, LLM summarization) ===")
|
||||
res = subprocess.run(["openclaw", "sessions", "--all-agents", "--json"], capture_output=True, text=True)
|
||||
if res.returncode != 0:
|
||||
print(f"Failed to list sessions: {res.stderr}")
|
||||
return
|
||||
|
||||
sessions = json.loads(res.stdout).get("sessions", [])
|
||||
compacted = 0
|
||||
skipped = 0
|
||||
|
||||
for s in sessions:
|
||||
key = s.get("key", "")
|
||||
agent = s.get("agentId") or "main"
|
||||
tokens = s.get("totalTokens") or 0
|
||||
sid = s.get("sessionId")
|
||||
|
||||
# Skip Tier A agents as they are handled via hard-reset
|
||||
if key in tier_a_keys:
|
||||
continue
|
||||
|
||||
# Target main sessions and all telegram sessions (topics and direct)
|
||||
is_target = (
|
||||
key == "agent:main:main" or
|
||||
":telegram:" in key or
|
||||
(key.startswith("agent:") and key.endswith(":main"))
|
||||
)
|
||||
if is_target and not key.endswith(":heartbeat"):
|
||||
ev_cnt, ev_bytes = get_session_metrics(agent, key, sid)
|
||||
needs_compaction = (
|
||||
tokens >= THRESHOLD_TOKENS or
|
||||
ev_bytes >= THRESHOLD_TRANSCRIPT_BYTES or
|
||||
(tokens == 0 and ev_cnt >= THRESHOLD_EVENT_COUNT)
|
||||
)
|
||||
if needs_compaction:
|
||||
stat_str = f"{tokens} tokens" if tokens else f"{ev_bytes//1024}KB / {ev_cnt} events"
|
||||
print(f"Compacting {agent} ({stat_str}) -> {key}...")
|
||||
c_res = subprocess.run(
|
||||
["openclaw", "sessions", "compact", key, "--agent", agent, "--timeout", str(RPC_TIMEOUT_MS)],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if c_res.returncode == 0:
|
||||
print(f" ✓ {c_res.stdout.strip()}")
|
||||
compacted += 1
|
||||
elif "Already compacted" in c_res.stderr or "Already compacted" in c_res.stdout:
|
||||
print(f" ✓ Already compacted.")
|
||||
skipped += 1
|
||||
else:
|
||||
print(f" ✗ {c_res.stderr.strip()}; retrying with --max-lines 80 fallback...")
|
||||
fb_res = subprocess.run(
|
||||
["openclaw", "sessions", "compact", key, "--agent", agent, "--max-lines", "80", "--timeout", str(RPC_TIMEOUT_MS)],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if fb_res.returncode == 0:
|
||||
print(f" ✓ (fallback) {fb_res.stdout.strip()}")
|
||||
compacted += 1
|
||||
else:
|
||||
print(f" ✗ Fallback failed: {fb_res.stderr.strip()}")
|
||||
time.sleep(5) # 5s pause to avoid rotational HDD I/O saturation
|
||||
else:
|
||||
skipped += 1
|
||||
|
||||
print(f"Compaction complete: {compacted} compacted, {skipped} skipped.")
|
||||
|
||||
def checkpoint_wal_journals():
|
||||
print("=== Checkpointing SQLite WAL journals across all agent databases ===")
|
||||
dbs = ["/home/node/.openclaw/state/openclaw.sqlite"] + glob.glob("/home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite")
|
||||
wal_freed = 0
|
||||
for db in dbs:
|
||||
wal = db + "-wal"
|
||||
if os.path.exists(wal):
|
||||
sz = os.path.getsize(wal)
|
||||
try:
|
||||
con = sqlite3.connect(db, timeout=30.0)
|
||||
con.execute("PRAGMA wal_checkpoint(TRUNCATE);")
|
||||
con.close()
|
||||
wal_freed += sz
|
||||
except Exception as e:
|
||||
print(f" ✗ Error checkpointing {db}: {e}")
|
||||
print(f"WAL Checkpoint complete: {wal_freed / (1024*1024):.2f} MB merged to disk.")
|
||||
|
||||
def vacuum_and_optimize():
|
||||
print("=== Running SQLite OPTIMIZE across all databases ===")
|
||||
dbs = ["/home/node/.openclaw/state/openclaw.sqlite"] + glob.glob("/home/node/.openclaw/agents/*/agent/openclaw-agent.sqlite")
|
||||
for db in dbs:
|
||||
try:
|
||||
con = sqlite3.connect(db, timeout=30.0)
|
||||
con.execute("PRAGMA optimize;")
|
||||
con.close()
|
||||
except Exception as e:
|
||||
print(f" ✗ Error optimizing {db}: {e}")
|
||||
print("SQLite optimization complete.")
|
||||
|
||||
def main():
|
||||
prewarm_page_cache()
|
||||
try:
|
||||
subprocess.run(["python3", "/home/node/.openclaw/scripts/recover_sessions.py"], check=False)
|
||||
except Exception:
|
||||
pass
|
||||
prune_stale_cron_sessions()
|
||||
reset_tier_a_sessions()
|
||||
compact_active_sessions()
|
||||
checkpoint_wal_journals()
|
||||
vacuum_and_optimize()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,38 @@
|
||||
# AGENTS.md — Kitchen e-Paper Display (`esphome_display_kitchen`)
|
||||
|
||||
> [!NOTE]
|
||||
> **Domain**: ESP32 microcontroller driving the e-Paper display in the kitchen.
|
||||
> **Source Location**: `projects/esphome_display_kitchen/`
|
||||
> **Integrations**: Home Assistant Native API (`https://hass.moritzgraf.de`), Prometheus metrics client.
|
||||
> **Hardware**: Waveshare e-Paper display on ESP32 (`nodemcu-32s`).
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Build, Flash & Messaging Tooling
|
||||
|
||||
- **Interactive Flashing Wizard**:
|
||||
```bash
|
||||
cd projects/esphome_display_kitchen
|
||||
uv run flash.py
|
||||
```
|
||||
Provides an interactive CLI to Validate, Compile, Upload, and view Logs for `esdisplay_kitchen.yaml`.
|
||||
- **Direct Terminal Messaging**:
|
||||
```bash
|
||||
cd projects/esphome_display_kitchen
|
||||
./send_message.sh "Your custom display message"
|
||||
```
|
||||
Uses the Home Assistant long-lived token from `k8s/home-assistant/` to set the `input_text.epaper_message` helper in Home Assistant, which instantly pushes updates to the screen.
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Rules & Standards
|
||||
|
||||
### 1. Secret Management
|
||||
* **NEVER** write plaintext Wi-Fi passwords, tokens, or API keys directly into YAML configurations.
|
||||
* **DO NOT** modify `.gitignore` to allow committing `.env` files.
|
||||
* Use ESPHome's `!env_var VAR_NAME` to inject environment variables at compile-time. Any new secrets must be declared in `.env.template` and configured locally in `.env`.
|
||||
|
||||
### 2. Configuration Standards
|
||||
* Board architecture: ESP32 (`nodemcu-32s` with `arduino` framework).
|
||||
* SNTP time synchronization configured via `time` component for clock rendering.
|
||||
* Sensor values (outdoor temperature, indoor climate) are dynamically pulled from Home Assistant entities.
|
||||
@@ -5,7 +5,7 @@ set -e
|
||||
CDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
|
||||
# Read the token from the home-assistant secrets folder
|
||||
TOKEN_FILE="${CDIR}/../k8s/home-assistant/tmp_long_lived_token.secret.yml"
|
||||
TOKEN_FILE="${CDIR}/../../k8s/home-assistant/tmp_long_lived_token.secret.yml"
|
||||
|
||||
if [ ! -f "$TOKEN_FILE" ]; then
|
||||
echo "Error: Home Assistant token file not found at $TOKEN_FILE."
|
||||
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.env
|
||||
.DS_Store
|
||||
@@ -0,0 +1,76 @@
|
||||
# AGENTS.md — Grabengasse 7 Home Portal (`grabengasse`)
|
||||
|
||||
> [!NOTE]
|
||||
> **Domain**: Local climate, environment, and smart home portal for Grabengasse 7.
|
||||
> **Live Production URL**: `https://grabengasse7.de`
|
||||
> **Kubernetes Manifests**: `k8s/grabengasse/` (Namespace: `grabengasse`)
|
||||
> **Images**:
|
||||
> - Frontend: `registry.haumdaucher.de/website/grabengasse_frontend:latest`
|
||||
> - Backend: `registry.haumdaucher.de/website/grabengasse_backend:latest`
|
||||
> **Source Location**: `projects/grabengasse/`
|
||||
|
||||
---
|
||||
|
||||
## 🏛️ Architecture & Tech Stack
|
||||
|
||||
1. **Frontend (`frontend/`)**:
|
||||
- **Framework**: Vue 3 (Composition API) + Vite + TypeScript.
|
||||
- **Styling**: Retro-chic aesthetic with pixel art accents, glassmorphic cards, and an optional CRT scanline toggle.
|
||||
- **Data Visualization**: Chart.js / Vue-Chartjs / Apexcharts for 24h temperature, humidity, wind, solar, and rain telemetry.
|
||||
- **Serving Runtime**: Nginx (`nginx:alpine`) in Docker.
|
||||
|
||||
2. **Backend (`backend/`)**:
|
||||
- **Framework**: FastAPI (Python 3.11).
|
||||
- **Telemetry Source**: Direct HTTP queries to cluster Prometheus (`http://prometheus-operator-kube-p-prometheus.monitoring.svc.cluster.local:9090`) aggregating metrics from the Shelly WS90 weather station.
|
||||
- **Endpoints**:
|
||||
- `GET /healthz`: Liveness and readiness probe.
|
||||
- `GET /api/v1/weather/current`: Live weather telemetry (temperature, humidity, wind speed, solar radiation, rain).
|
||||
- `GET /api/v1/weather/history`: Historical timeseries for 24h graphing.
|
||||
|
||||
3. **Kubernetes Ingress & Routing (`k8s/grabengasse/`)**:
|
||||
- Ingress Host: `grabengasse7.de` with Traefik ingress controller and `cert-manager` TLS.
|
||||
- `/` → `grabengasse-frontend` (Port 80)
|
||||
- `/api` → `grabengasse-backend` (Port 8000)
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Build & Deployment Commands
|
||||
|
||||
```bash
|
||||
cd projects/grabengasse
|
||||
make compile # Build frontend static assets (npm run build)
|
||||
make build # Build both frontend and backend Docker images (linux/amd64)
|
||||
make push # Push images to registry.haumdaucher.de
|
||||
make deploy # Apply k8s manifests and rollout restart pods
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 Repository Structure
|
||||
|
||||
```
|
||||
projects/grabengasse/
|
||||
├── Makefile # Coordinated multi-service build and deploy
|
||||
├── AGENTS.md # This handbook
|
||||
├── README.md # Project overview
|
||||
├── frontend/ # Vue 3 SPA
|
||||
│ ├── Dockerfile
|
||||
│ ├── Makefile
|
||||
│ ├── nginx.conf
|
||||
│ ├── package.json
|
||||
│ ├── src/
|
||||
│ │ ├── App.vue # Weather dashboard layout & state
|
||||
│ │ ├── components/ # Weather gauges, telemetry cards & retro CRT overlay
|
||||
│ │ └── scripts/ # generate_icons.py (Pixel art icon generator)
|
||||
│ └── public/ # Static icons, manifest, and service worker
|
||||
└── backend/ # FastAPI microservice
|
||||
├── Dockerfile
|
||||
├── Makefile
|
||||
├── requirements.txt
|
||||
├── main.py # FastAPI application entrypoint
|
||||
├── config.py # CORS & environment configuration
|
||||
├── routers/
|
||||
│ └── weather.py # /api/v1/weather routes
|
||||
└── services/
|
||||
└── prometheus.py # Prometheus query client
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
REGISTRY ?= registry.haumdaucher.de
|
||||
FRONTEND_IMAGE ?= $(REGISTRY)/website/grabengasse_frontend:latest
|
||||
BACKEND_IMAGE ?= $(REGISTRY)/website/grabengasse_backend:latest
|
||||
NAMESPACE ?= grabengasse
|
||||
|
||||
.PHONY: all compile dev build push deploy rollout build-frontend build-backend push-frontend push-backend
|
||||
|
||||
all: compile build push deploy
|
||||
|
||||
compile:
|
||||
cd frontend && npm run build
|
||||
|
||||
dev-frontend:
|
||||
cd frontend && npm run dev
|
||||
|
||||
dev-backend:
|
||||
cd backend && uvicorn main:app --reload
|
||||
|
||||
build: build-frontend build-backend
|
||||
|
||||
build-frontend:
|
||||
cd frontend && docker build --platform linux/amd64 -t $(FRONTEND_IMAGE) .
|
||||
|
||||
build-backend:
|
||||
cd backend && docker build --platform linux/amd64 -t $(BACKEND_IMAGE) .
|
||||
|
||||
push: push-frontend push-backend
|
||||
|
||||
push-frontend:
|
||||
docker push $(FRONTEND_IMAGE)
|
||||
|
||||
push-backend:
|
||||
docker push $(BACKEND_IMAGE)
|
||||
|
||||
deploy:
|
||||
kubectl apply -f ../../k8s/grabengasse/backend.yaml -f ../../k8s/grabengasse/frontend.yaml -f ../../k8s/grabengasse/ingress.yaml
|
||||
$(MAKE) rollout
|
||||
|
||||
rollout:
|
||||
kubectl rollout restart deployment/grabengasse-frontend -n $(NAMESPACE)
|
||||
kubectl rollout restart deployment/grabengasse-backend -n $(NAMESPACE)
|
||||
kubectl rollout status deployment/grabengasse-frontend -n $(NAMESPACE)
|
||||
kubectl rollout status deployment/grabengasse-backend -n $(NAMESPACE)
|
||||
@@ -0,0 +1,12 @@
|
||||
# Grabengasse 7 Home Portal 🌦️
|
||||
|
||||
Climate and environmental monitoring web dashboard for Grabengasse 7 (`https://grabengasse7.de`).
|
||||
|
||||
## Architecture
|
||||
|
||||
- **`frontend/`**: Vue 3 + TypeScript SPA displaying real-time telemetry from the Shelly WS90 outdoor weather station.
|
||||
- **`backend/`**: FastAPI backend fetching Prometheus metric aggregations for temperature, humidity, rain, wind, and solar irradiance.
|
||||
|
||||
## Development & Deployment
|
||||
|
||||
See [AGENTS.md](AGENTS.md) for architecture details and the [Makefile](Makefile) for standard build commands (`make compile`, `make build`, `make deploy`).
|
||||
|
Before Width: | Height: | Size: 947 B After Width: | Height: | Size: 947 B |
|
Before Width: | Height: | Size: 436 B After Width: | Height: | Size: 436 B |
|
Before Width: | Height: | Size: 1019 B After Width: | Height: | Size: 1019 B |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |