refactor(structure): reorganize monorepo with projects, terraform hierarchy, and strict k8s namespace rules

This commit is contained in:
Moritz Graf
2026-10-12 13:49:41 +02:00
parent 4831147087
commit 8f01b75bd6
4766 changed files with 551 additions and 91 deletions
+39 -20
View File
@@ -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**:
+20 -23
View File
@@ -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).
-20
View File
@@ -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.
+3 -2
View File
@@ -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
+7 -7
View File
@@ -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
+2 -2
View File
@@ -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
```
+5 -5
View File
@@ -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)
+1 -1
View File
@@ -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`.*
---
+275
View File
@@ -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."
+6
View File
@@ -0,0 +1,6 @@
node_modules/
dist/
__pycache__/
*.pyc
.env
.DS_Store
+76
View File
@@ -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.
- `/` &rarr; `grabengasse-frontend` (Port 80)
- `/api` &rarr; `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
```
+43
View File
@@ -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)
+12
View File
@@ -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

Some files were not shown because too many files have changed in this diff Show More