feat(openclaw): add shopping, supermarket deals, geizhals, and ebay MCP tools
This commit is contained in:
@@ -160,6 +160,27 @@ velero backup get
|
||||
```
|
||||
|
||||
|
||||
## authentik (auth)
|
||||
|
||||
Authentik is the central SSO and identity provider deployed at [auth.haumdaucher.de](https://auth.haumdaucher.de).
|
||||
See full documentation in [k8s/auth/README.md](./auth/README.md) and [k8s/auth/MACHINE_AUTH.md](./auth/MACHINE_AUTH.md).
|
||||
|
||||
```sh
|
||||
# 1. Create namespace & CNPG PostgreSQL cluster
|
||||
kubectl create namespace auth
|
||||
kubectl apply -f auth/authentik-db.yaml
|
||||
|
||||
# 2. Deploy Authentik via Helm
|
||||
helm repo add authentik https://charts.goauthentik.io
|
||||
helm repo update
|
||||
helm upgrade --install authentik authentik/authentik \
|
||||
--namespace auth \
|
||||
-f auth/authentik.secret.values.yaml
|
||||
|
||||
# 3. Deploy Traefik ForwardAuth Middleware
|
||||
kubectl apply -f auth/traefik-forwardauth-middleware.yaml
|
||||
```
|
||||
|
||||
## coder
|
||||
|
||||
Coder is a code server see [https://coder.com/docs/v2/latest/install/kubernetes](https://coder.com/docs/v2/latest/install/kubernetes).
|
||||
@@ -841,3 +862,47 @@ helm upgrade --install zigbee2mqtt zigbee2mqtt/zigbee2mqtt --create-namespace -n
|
||||
```
|
||||
|
||||
To modify configurations (like `devices`, `throttle`, or `mqtt`), edit `zigbee/zigbee2mqtt.secret.values.yaml` and re-run the `helm upgrade` command. The chart automatically handles merging these values into the persistent volume on startup.
|
||||
|
||||
## Grabengasse 7 Website (`grabengasse7.de`)
|
||||
|
||||
Grabengasse 7 home website and backend service deployed in the `grabengasse` namespace. Exposes outdoor weather metrics from the Shelly WS90 station with interactive 24h charts.
|
||||
|
||||
### Build and Push Container Images
|
||||
|
||||
```bash
|
||||
# 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 frontend image
|
||||
cd ../grabengasse_frontend
|
||||
make all
|
||||
```
|
||||
|
||||
### Deploy to Kubernetes
|
||||
|
||||
```bash
|
||||
# Create namespace and registry secret
|
||||
kubectl apply -f k8s/grabengasse/namespace.yaml
|
||||
kubectl apply -f k8s/grabengasse/registry.secret.yaml
|
||||
|
||||
# Deploy backend, frontend, and ingress
|
||||
kubectl apply -f k8s/grabengasse/backend.yaml
|
||||
kubectl apply -f k8s/grabengasse/frontend.yaml
|
||||
kubectl apply -f k8s/grabengasse/ingress.yaml
|
||||
```
|
||||
|
||||
### Verification
|
||||
|
||||
```bash
|
||||
# Verify pod statuses
|
||||
kubectl get pods -n grabengasse
|
||||
|
||||
# Check backend health
|
||||
curl -k https://grabengasse7.de/api/v1/healthz
|
||||
curl -k https://grabengasse7.de/api/v1/weather/current
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# Machine & AI Assistant Authentication Guide
|
||||
|
||||
This guide describes how to configure programmatic, machine-to-machine, and AI assistant authentication across services protected by **Authentik** and **Traefik ForwardAuth**.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Use Case
|
||||
When an AI assistant (or automation script, CLI tool, or cron job) needs to:
|
||||
* Inspect Zigbee2MQTT logs or device states via `https://zigbee2mqtt.moritzgraf.de`
|
||||
* Scrape or query metrics from `https://prometheus.haumdaucher.de`
|
||||
* Trigger or query APIs protected behind the Traefik ForwardAuth gateway
|
||||
|
||||
Instead of interactive browser logins or hardcoding static basic auth credentials into individual ingress configs, Authentik supports **Service Accounts & API Tokens**.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Setup Instructions
|
||||
|
||||
### 1. Create a Service Account in Authentik
|
||||
1. Navigate to **Directory > Users** in Authentik (`https://auth.haumdaucher.de`).
|
||||
2. Click **Create Service Account**:
|
||||
* **Username**: `ai-assistant` (or `mopbot`, `cli-agent`)
|
||||
* **Create API Token**: Check this box.
|
||||
* **Token Expiration**: Set to never expire (or desired TTL).
|
||||
3. Copy the generated **Token Secret** (e.g. `ak_tok_xxxx...`).
|
||||
|
||||
### 2. Grant Access Permissions
|
||||
1. Go to **Directory > Groups**.
|
||||
2. Add the `ai-assistant` service account to the appropriate group (e.g. `infra-admins` or `zigbee-readers`).
|
||||
3. Ensure the Application/Proxy Provider policy allows this group.
|
||||
|
||||
---
|
||||
|
||||
## 💻 Making Authenticated API Calls
|
||||
|
||||
### HTTP Bearer Token (Recommended)
|
||||
Pass the API token in the `Authorization` header:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <your-authentik-token>" \
|
||||
https://zigbee2mqtt.moritzgraf.de/api/bridge/info
|
||||
```
|
||||
|
||||
When Traefik ForwardAuth queries Authentik with this header, Authentik detects the Bearer token, authenticates the `ai-assistant` service account, checks permissions, and responds with HTTP `200 OK`. Traefik then proxies the request directly to the backend.
|
||||
|
||||
### Python / Scripting Example
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
AUTH_TOKEN = "your-authentik-token"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {AUTH_TOKEN}",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
|
||||
response = requests.get("https://zigbee2mqtt.moritzgraf.de/api/bridge/devices", headers=headers)
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Rotating & Revoking Tokens
|
||||
* To rotate a token, navigate to **Directory > Tokens and App passwords**.
|
||||
* You can view active tokens, inspect last-used timestamps, and revoke compromised tokens instantly with one click without restarting any Kubernetes pods or modifying ingress secrets.
|
||||
@@ -0,0 +1,188 @@
|
||||
# Authentik Identity & Access Management (SSO)
|
||||
|
||||
Centralized authentication and identity provider for the `haumdaucher` Kubernetes cluster.
|
||||
|
||||
* **Domain**: `https://auth.haumdaucher.de`
|
||||
* **Helm Chart**: `authentik/authentik` (Version 2026.8.0)
|
||||
* **Database**: CloudNative-PG (`cnpg`) PostgreSQL cluster (`authentik-db`)
|
||||
* **Ingress**: Traefik with Let's Encrypt TLS certificate (`auth-haumdaucher-de`)
|
||||
* **Capabilities**: OIDC/OAuth2 Provider, Traefik ForwardAuth, 2FA (WebAuthn/Passkey, TOTP), User Invitations, Machine/AI Service Accounts.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture Layout
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Client[Browser / User] -->|HTTPS| Traefik[Traefik Ingress]
|
||||
AI[AI Assistant / CLI Script] -->|HTTPS + Bearer Token| Traefik
|
||||
|
||||
subgraph "ForwardAuth Ingresses"
|
||||
Traefik -->|ForwardAuth Check| ForwardAuthMW[Traefik ForwardAuth Middleware]
|
||||
ForwardAuthMW -->|Verify Session or Token| AuthServer[Authentik Server]
|
||||
Traefik -->|Allow| Z2M[Zigbee2MQTT]
|
||||
Traefik -->|Allow| Prom[Prometheus & Alertmanager]
|
||||
Traefik -->|Allow| PMA[phpMyAdmin]
|
||||
Traefik -->|Allow| OC[OpenClaw]
|
||||
Traefik -->|Allow| N8N[n8n Web UI]
|
||||
end
|
||||
|
||||
subgraph "Native OIDC Workloads"
|
||||
Grafana[Grafana] -->|OIDC Auth Code Flow| AuthServer
|
||||
Coder[Coder v2] -->|OIDC Auth| AuthServer
|
||||
Gitea[Gitea] -->|OIDC Auth| AuthServer
|
||||
Nextcloud[Nextcloud user_oidc] -->|OIDC Auth| AuthServer
|
||||
MinIO[MinIO Console] -->|OIDC Auth| AuthServer
|
||||
end
|
||||
|
||||
subgraph "auth Namespace"
|
||||
AuthServer --> DB[(CNPG Postgres: authentik-db)]
|
||||
AuthWorker[Authentik Worker] --> DB
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Deployment Instructions
|
||||
|
||||
### 1. Create Namespace & Provision CNPG Database
|
||||
|
||||
```bash
|
||||
# Create auth namespace
|
||||
kubectl create namespace auth
|
||||
|
||||
# Apply CNPG PostgreSQL cluster
|
||||
kubectl apply -f k8s/auth/authentik-db.yaml
|
||||
|
||||
# Verify PostgreSQL cluster is ready (Status: Cluster in healthy state)
|
||||
kubectl get cluster -n auth authentik-db
|
||||
```
|
||||
|
||||
### 2. Deploy Authentik via Helm
|
||||
|
||||
```bash
|
||||
# Add Authentik repository
|
||||
helm repo add authentik https://charts.goauthentik.io
|
||||
helm repo update
|
||||
|
||||
# Install or upgrade Authentik release
|
||||
helm upgrade --install authentik authentik/authentik \
|
||||
--namespace auth \
|
||||
-f k8s/auth/authentik.secret.values.yaml
|
||||
```
|
||||
|
||||
### 3. Deploy Traefik ForwardAuth Middleware
|
||||
|
||||
```bash
|
||||
kubectl apply -f k8s/auth/traefik-forwardauth-middleware.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Initial Setup & Admin Onboarding
|
||||
|
||||
1. Open your browser and navigate to:
|
||||
```
|
||||
https://auth.haumdaucher.de/if/flow/initial-setup/
|
||||
```
|
||||
2. Set a secure password for the default `akadmin` account.
|
||||
3. Configure your admin user profile and optionally enroll a WebAuthn / Passkey / TOTP 2FA device.
|
||||
|
||||
---
|
||||
|
||||
## 👥 User Management & Invitations
|
||||
|
||||
### Creating Users & Sending Invites
|
||||
1. Navigate to **Directory > Users** in the Authentik Admin interface.
|
||||
2. You can create users manually or upload/provision users in batches.
|
||||
3. To send enrollment links, go to **Directory > Invitations**:
|
||||
* Click **Create**.
|
||||
* Set the target email and assign user groups (e.g. `family`, `friends`, `admins`).
|
||||
* Copy the generated invitation URL or send it to the user.
|
||||
|
||||
### Group Permissions
|
||||
* `authentik Admins`: Full administrative control over Authentik.
|
||||
* `infra-admins`: Access to infrastructure tools (Prometheus, Alertmanager, phpMyAdmin, Zigbee2MQTT).
|
||||
* `users`: Standard user group with access to shared web services (Grafana dashboards, Coder, Nextcloud, Gitea).
|
||||
|
||||
👉 **For the complete guide on Terraform user management, bulk Excel/CSV imports, custom email templates, and RBAC/PBAC access limiting, see [USER_MANAGEMENT.md](./USER_MANAGEMENT.md).**
|
||||
|
||||
---
|
||||
|
||||
## 🤖 Machine & AI Agent Access (Programmatic Authentication)
|
||||
|
||||
For automated tools, scripts, or AI assistants that need to inspect protected applications (e.g. querying Zigbee2MQTT API/logs or Prometheus metrics):
|
||||
|
||||
1. **Create a Service Account**:
|
||||
* In Authentik Admin, navigate to **Directory > Users**.
|
||||
* Click **Create Service Account** (e.g. `ai-agent` or `automation-bot`).
|
||||
* Create a permanent API token for the service account.
|
||||
2. **Accessing Protected Endpoints**:
|
||||
* Pass the token in the HTTP `Authorization` header:
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <your-authentik-token>" https://zigbee2mqtt.moritzgraf.de/api/...
|
||||
```
|
||||
* Authentik's ForwardAuth middleware validates the Bearer token and allows the request without redirecting to a web login page.
|
||||
|
||||
See [MACHINE_AUTH.md](./MACHINE_AUTH.md) for full details.
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Protecting Applications (Rollout Guide)
|
||||
|
||||
### Pattern 1: Traefik ForwardAuth (Proxy Protection)
|
||||
Use this for applications without native OIDC (e.g. Zigbee2MQTT, Prometheus, phpMyAdmin, OpenClaw).
|
||||
|
||||
1. In Authentik Admin, go to **Applications > Providers**:
|
||||
* Create a **Proxy Provider**.
|
||||
* Name: `<App Name> Proxy` (e.g. `Zigbee2MQTT Proxy`).
|
||||
* Mode: `Forward auth (single application)`.
|
||||
* External Host: `https://<app-domain>` (e.g. `https://zigbee2mqtt.moritzgraf.de`).
|
||||
2. Go to **Applications > Applications**:
|
||||
* Create Application, link the Provider, and set access policies/groups.
|
||||
3. Add the middleware annotation to the application's Kubernetes Ingress:
|
||||
```yaml
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.middlewares: auth-authentik-forwardauth@kubernetescrd
|
||||
```
|
||||
|
||||
### Pattern 2: Native OIDC SSO
|
||||
Use this for applications supporting OpenID Connect (Grafana, Coder, Gitea, Nextcloud).
|
||||
|
||||
1. In Authentik Admin, create an **OAuth2/OpenID Provider**:
|
||||
* Name: `<App Name> OIDC`
|
||||
* Client Type: `Confidential`
|
||||
* Redirect URI: e.g. `https://grafana.haumdaucher.de/login/generic_oauth`
|
||||
2. Create an **Application** in Authentik and assign it to the Provider.
|
||||
3. Configure the client credentials (`client_id` and `client_secret`) in the application's Helm values / ConfigMap.
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Branding & Haumdaucher UI Theme
|
||||
|
||||
The Authentik login screen is customized with a **Haumdaucher** glassmorphism theme matching `haumdaucher.de`.
|
||||
|
||||
* **Styling Highlights**:
|
||||
* **Glassmorphic Card**: Frosted glass panel (`backdrop-filter: blur(20px)`), smooth rounded corners (`24px`), and elegant ambient background aura.
|
||||
* **Typography**: Clean, modern typography using Google Font **Outfit**.
|
||||
* **Logo & Entrance Animation**: Dynamic 3D scale-and-fade entry animation for the Haumdaucher crest logo (`https://haumdaucher.de/images/logo_classic.png`).
|
||||
* **Dual Light & Dark Theme**: Automatic adaptation with dark mode styling featuring obsidian surfaces, gold accents (`#d4af37`), and glowing tactile buttons.
|
||||
* **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`:
|
||||
|
||||
```bash
|
||||
cd terraform
|
||||
terraform apply -target=authentik_brand.haumdaucher
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💾 Backup & Disaster Recovery
|
||||
|
||||
* **Database**: Managed by CloudNative-PG on `openebs-hostpath` persistent storage.
|
||||
* **Velero Integration**: The `auth` namespace is backed up during daily/weekly Velero schedules:
|
||||
```bash
|
||||
velero backup create auth-backup-$(date +%Y%m%d) --include-namespaces auth --wait
|
||||
```
|
||||
@@ -0,0 +1,629 @@
|
||||
# Authentik User Management: Complete Roundup & Operations Guide
|
||||
|
||||
This guide covers everything you need to know about managing users in Authentik for the **Haumdaucher** infrastructure (`https://auth.haumdaucher.de`): from manual creation and invitation workflows to Terraform IaC, custom email branding, fine-grained access control, handling custom attributes (e.g., Vereins-Mitgliedsnummer, second email), and bulk-importing users from an Excel / CSV spreadsheet.
|
||||
|
||||
---
|
||||
|
||||
## 📑 Table of Contents
|
||||
|
||||
1. [Executive Summary: Quick Answers](#-executive-summary-quick-answers)
|
||||
2. [Method 1: Manual Web UI & Invitation Links](#-method-1-manual-web-ui--invitation-links)
|
||||
3. [Method 2: User Provisioning via Terraform](#-method-2-user-provisioning-via-terraform)
|
||||
4. [Method 3: Bulk Import from Excel / CSV (Python Script)](#-method-3-bulk-import-from-excel--csv-python-script)
|
||||
5. [Customizing Registration & Onboarding Emails](#-customizing-registration--onboarding-emails)
|
||||
6. [Access Control & Authorization Policies (Limiting App Access)](#-access-control--authorization-policies-limiting-app-access)
|
||||
7. [Custom Attributes & OIDC Claim Mappings](#-custom-attributes--oidc-claim-mappings)
|
||||
8. [Production Checklist & Best Practices](#-production-checklist--best-practices)
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Executive Summary: Quick Answers
|
||||
|
||||
| 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 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. |
|
||||
|
||||
---
|
||||
|
||||
## 👤 Method 1: Manual Web UI & Invitation Links
|
||||
|
||||
Authentik offers two UI methods to onboard users:
|
||||
|
||||
### Option A: Direct User Creation (Admin sets password or triggers reset)
|
||||
1. Go to `https://auth.haumdaucher.de` and log in with your admin account.
|
||||
2. Navigate to **Directory > Users** and click **Create**.
|
||||
3. Fill in:
|
||||
* **Username**: `max.mustermann`
|
||||
* **Name**: `Max Mustermann`
|
||||
* **Email**: `max@example.com`
|
||||
* **Type**: `Internal`
|
||||
* **Groups**: Select groups (e.g., `users`, `verein-mitglieder`).
|
||||
* **Attributes (YAML/JSON)**:
|
||||
```json
|
||||
{
|
||||
"second_email": "max.privat@gmail.com",
|
||||
"haumdaucher_mitgliedsnummer": "HM-2024-042",
|
||||
"phone": "+49 170 1234567"
|
||||
}
|
||||
```
|
||||
4. Click **Create**.
|
||||
5. To set credentials: Click on the user -> **Actions** -> **Set Password** (or **Explicitly trigger password reset flow** so the user receives a link via email).
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Troubleshooting: "Flow does not apply to current user"
|
||||
|
||||
If you encounter this screen when opening an invite link:
|
||||
|
||||
> **"Flow does not apply to current user."**
|
||||
|
||||
### Why this happens:
|
||||
1. **Wrong Flow Selected (`default-source-enrollment`)**:
|
||||
In your URL (`/if/flow/default-source-enrollment/?itoken=...`), the flow used was `default-source-enrollment`. That flow is strictly designed for external identity providers (OAuth/SAML like Google/GitHub) and binds a policy that checks for an active external source in the context. When accessed directly with an `?itoken=...`, Authentik evaluates this policy, fails, and displays this error.
|
||||
2. **Missing Invitation Stage**:
|
||||
Standard enrollment flows reject direct token access unless an **`Invitation Stage`** is bound to the beginning of the flow to validate `?itoken=...`.
|
||||
3. **Active Admin Session**:
|
||||
If an administrator is currently logged in to the browser and clicks the enrollment link, flows with an "unauthenticated-only" policy will reject the session. Always test invitation URLs in an **Incognito / Private window**.
|
||||
|
||||
---
|
||||
|
||||
### Solution: The Haumdaucher Custom Invitation Flow (`haumdaucher-invitation-flow`)
|
||||
|
||||
We provision a dedicated, streamlined flow in `terraform/authentik.tf`:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Link[Invite Link with ?itoken=...] --> Stage1[1. Invitation Stage<br/>Validates Token]
|
||||
Stage1 --> Stage2[2. Prompt Stage<br/>User inputs Username, Name, Password]
|
||||
Stage2 --> Stage3[3. User Write Stage<br/>Writes to DB & assigns Groups]
|
||||
Stage3 --> Stage4[4. User Login Stage<br/>Logs user into session]
|
||||
Stage4 --> Stage5[5. Redirect Stage<br/>Redirects to haumdaucher.de]
|
||||
```
|
||||
|
||||
#### Flow Behavior (Tailored to your setup):
|
||||
* **User Inputs**: Prompts the user to choose their preferred **Username**, **Full Name**, and **Password** (with confirmation).
|
||||
* **Email & Custom Attributes**: Pre-filled from the admin invitation (e.g. `haumdaucher_mitgliedsnummer`, `second_email`).
|
||||
* **MFA / 2FA**: Optional. The user registers immediately with their password and can enroll Passkeys/TOTP anytime in their profile settings.
|
||||
* **Automatic Redirect**: Upon completing registration, the user is automatically logged in and redirected to **`https://haumdaucher.de`**.
|
||||
|
||||
---
|
||||
|
||||
### How to Create an Invitation using the Custom Flow
|
||||
|
||||
1. Go to `https://auth.haumdaucher.de` > **Directory > Invitations**.
|
||||
2. Click **Create** (or **New Invitation**).
|
||||
3. Set the fields:
|
||||
* **Name**: `Invite - Max Mustermann`
|
||||
* **Expires**: Set expiration (e.g., 7 days).
|
||||
* **Flow**: Select **`Haumdaucher Invitation Flow`** (`haumdaucher-invitation-flow`).
|
||||
* **Single use**: Checked (`true`).
|
||||
* **Fixed Data / Attributes (JSON)**:
|
||||
```json
|
||||
{
|
||||
"email": "max@example.com",
|
||||
"haumdaucher_mitgliedsnummer": "HM-2024-042",
|
||||
"second_email": "max.privat@gmail.com"
|
||||
}
|
||||
```
|
||||
4. Click **Create**.
|
||||
5. Copy the generated URL:
|
||||
```
|
||||
https://auth.haumdaucher.de/if/flow/haumdaucher-invitation-flow/?itoken=<generated-token>
|
||||
```
|
||||
6. Send the link to the user. When they open it, they will see the branded Haumdaucher registration form asking for their Username, Name, and Password!
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Method 2: User Provisioning via Terraform
|
||||
|
||||
You can manage users, groups, and permissions directly in `terraform/authentik.tf`.
|
||||
|
||||
### 1. Single User Definition
|
||||
```hcl
|
||||
# Group for club members
|
||||
resource "authentik_group" "verein_mitglieder" {
|
||||
name = "verein-mitglieder"
|
||||
is_superuser = false
|
||||
}
|
||||
|
||||
# User with custom attributes
|
||||
resource "authentik_user" "max_mustermann" {
|
||||
username = "max.mustermann"
|
||||
name = "Max Mustermann"
|
||||
email = "max@example.com"
|
||||
type = "internal"
|
||||
is_active = true
|
||||
groups = [
|
||||
authentik_group.users.id,
|
||||
authentik_group.verein_mitglieder.id
|
||||
]
|
||||
attributes = jsonencode({
|
||||
"second_email" = "max.privat@gmail.com",
|
||||
"haumdaucher_mitgliedsnummer" = "HM-2024-042",
|
||||
"eintrittsdatum" = "2024-01-15",
|
||||
"status" = "aktiv"
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Batch Provisioning from CSV in Terraform
|
||||
If you maintain users in a CSV file (`terraform/users.csv`):
|
||||
|
||||
```csv
|
||||
username,name,email,second_email,mitgliedsnummer,is_admin
|
||||
max.mustermann,Max Mustermann,max@example.com,max.privat@gmail.com,HM-2024-042,false
|
||||
anna.schmidt,Anna Schmidt,anna@example.com,anna.work@company.de,HM-2024-043,true
|
||||
```
|
||||
|
||||
In `terraform/authentik.tf`:
|
||||
```hcl
|
||||
locals {
|
||||
csv_users = csvdecode(file("${path.module}/users.csv"))
|
||||
}
|
||||
|
||||
resource "authentik_user" "csv_members" {
|
||||
for_each = { for u in local.csv_users : u.username => u }
|
||||
|
||||
username = each.value.username
|
||||
name = each.value.name
|
||||
email = each.value.email
|
||||
type = "internal"
|
||||
is_active = true
|
||||
groups = compact([
|
||||
authentik_group.users.id,
|
||||
authentik_group.verein_mitglieder.id,
|
||||
each.value.is_admin == "true" ? authentik_group.infra_admins.id : null
|
||||
])
|
||||
attributes = jsonencode({
|
||||
"second_email" = each.value.second_email,
|
||||
"haumdaucher_mitgliedsnummer" = each.value.mitgliedsnummer
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### ⚖️ Trade-offs: When to use Terraform for Users
|
||||
* **Pros**: Version-controlled in Git, declarative, reproducible, ideal for admins and service accounts (`ai_assistant`).
|
||||
* **Cons / Considerations**: Terraform cannot trigger interactive password onboarding emails out of the box; passwords must be set via recovery flow or first login reset.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Method 3: Bulk Import from Excel / CSV (Python Script)
|
||||
|
||||
If you have an Excel file (`mitglieder.xlsx`) or CSV, using Authentik's REST API is the cleanest approach because it can **create users, attach custom attributes, and automatically email them an invitation link to set their own password**.
|
||||
|
||||
### Ready-to-Run Python Import Script
|
||||
Save this script (e.g. `k8s/auth/scripts/import_users_excel.py`):
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Bulk User Importer for Authentik (auth.haumdaucher.de)
|
||||
Reads an Excel (.xlsx) or CSV file and creates users with custom attributes and invitation links.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import requests
|
||||
import pandas as pd
|
||||
|
||||
# Configuration
|
||||
AUTHENTIK_URL = os.getenv("AUTHENTIK_URL", "https://auth.haumdaucher.de")
|
||||
AUTHENTIK_TOKEN = os.getenv("AUTHENTIK_TOKEN") # API Token (e.g. ai_assistant_token)
|
||||
|
||||
if not AUTHENTIK_TOKEN:
|
||||
print("❌ Error: AUTHENTIK_TOKEN environment variable is required.")
|
||||
sys.exit(1)
|
||||
|
||||
HEADERS = {
|
||||
"Authorization": f"Bearer {AUTHENTIK_TOKEN}",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
|
||||
def get_or_create_group(group_name: str) -> str:
|
||||
"""Fetch group ID or create if it doesn't exist."""
|
||||
res = requests.get(f"{AUTHENTIK_URL}/api/v3/core/groups/?name={group_name}", headers=HEADERS)
|
||||
res.raise_for_status()
|
||||
results = res.json().get("results", [])
|
||||
if results:
|
||||
return results[0]["pk"]
|
||||
|
||||
# Create group
|
||||
create_res = requests.post(f"{AUTHENTIK_URL}/api/v3/core/groups/", headers=HEADERS, json={"name": group_name})
|
||||
create_res.raise_for_status()
|
||||
return create_res.json()["pk"]
|
||||
|
||||
def import_users_from_file(file_path: str, send_invitation: bool = True):
|
||||
# Read Excel or CSV
|
||||
if file_path.endswith((".xlsx", ".xls")):
|
||||
df = pd.read_excel(file_path)
|
||||
else:
|
||||
df = pd.read_csv(file_path)
|
||||
|
||||
# Standardize column names (lowercase & stripped)
|
||||
df.columns = [c.strip().lower() for c in df.columns]
|
||||
|
||||
print(f"📋 Found {len(df)} rows to process.")
|
||||
|
||||
users_group_pk = get_or_create_group("users")
|
||||
verein_group_pk = get_or_create_group("verein-mitglieder")
|
||||
|
||||
for idx, row in df.iterrows():
|
||||
name = str(row.get("name", "")).strip()
|
||||
email = str(row.get("email", "")).strip()
|
||||
username = str(row.get("username", "")).strip() or email.split("@")[0].lower()
|
||||
second_email = str(row.get("second_email", row.get("zweite_email", ""))).strip()
|
||||
mitgliedsnummer = str(row.get("mitgliedsnummer", row.get("verein_mitgliedsnummer", ""))).strip()
|
||||
|
||||
if not email or email == "nan":
|
||||
print(f"⚠️ Skipping row {idx+1}: missing email")
|
||||
continue
|
||||
|
||||
# Prepare user payload with custom attributes
|
||||
user_payload = {
|
||||
"username": username,
|
||||
"name": name if name != "nan" else username,
|
||||
"email": email,
|
||||
"type": "internal",
|
||||
"is_active": True,
|
||||
"groups": [users_group_pk, verein_group_pk],
|
||||
"attributes": {
|
||||
"second_email": second_email if second_email != "nan" else "",
|
||||
"haumdaucher_mitgliedsnummer": mitgliedsnummer if mitgliedsnummer != "nan" else "",
|
||||
"source": "excel_import"
|
||||
}
|
||||
}
|
||||
|
||||
# 1. Create or Update User in Authentik
|
||||
res = requests.get(f"{AUTHENTIK_URL}/api/v3/core/users/?username={username}", headers=HEADERS)
|
||||
res.raise_for_status()
|
||||
existing = res.json().get("results", [])
|
||||
|
||||
if existing:
|
||||
user_pk = existing[0]["pk"]
|
||||
patch_res = requests.patch(f"{AUTHENTIK_URL}/api/v3/core/users/{user_pk}/", headers=HEADERS, json=user_payload)
|
||||
patch_res.raise_for_status()
|
||||
print(f"🔄 Updated existing user: {username} ({email})")
|
||||
else:
|
||||
create_res = requests.post(f"{AUTHENTIK_URL}/api/v3/core/users/", headers=HEADERS, json=user_payload)
|
||||
if not create_res.ok:
|
||||
print(f"❌ Failed to create {username}: {create_res.text}")
|
||||
continue
|
||||
user_pk = create_res.json()["pk"]
|
||||
print(f"✅ Created user: {username} ({email}) [ID: {user_pk}]")
|
||||
|
||||
# 2. Generate Invitation / Password Reset Link
|
||||
if send_invitation:
|
||||
invitation_payload = {
|
||||
"name": f"Onboarding {username}",
|
||||
"single_use": True,
|
||||
"fixed_data": {
|
||||
"email": email,
|
||||
"username": username
|
||||
}
|
||||
}
|
||||
inv_res = requests.post(f"{AUTHENTIK_URL}/api/v3/stages/invitation/invitations/", headers=HEADERS, json=invitation_payload)
|
||||
if inv_res.ok:
|
||||
inv_key = inv_res.json()["pk"]
|
||||
link = f"{AUTHENTIK_URL}/if/flow/default-enrollment-flow/?itoken={inv_key}"
|
||||
print(f" ✉️ Invitation Link: {link}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 import_users_excel.py <path-to-excel-or-csv>")
|
||||
sys.exit(1)
|
||||
import_users_from_file(sys.argv[1])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✉️ Customizing Registration & Onboarding Emails
|
||||
|
||||
When an admin creates a user or triggers an enrollment/password recovery, Authentik sends transactional emails. You can customize their sender address, subject line, and full HTML appearance.
|
||||
|
||||
### 1. SMTP & Sender Settings
|
||||
Authentik sends emails using the SMTP configuration defined in `k8s/auth/authentik.secret.values.yaml` or global environment variables:
|
||||
* `AUTHENTIK_EMAIL__HOST`: `smtp.gmail.com` (or Google Workspace / OVH relay)
|
||||
* `AUTHENTIK_EMAIL__PORT`: `587`
|
||||
* `AUTHENTIK_EMAIL__USE_TLS`: `true`
|
||||
* `AUTHENTIK_EMAIL__USERNAME`: `service@haumdaucher.de`
|
||||
* `AUTHENTIK_EMAIL__PASSWORD`: `<app-password>`
|
||||
* `AUTHENTIK_EMAIL__FROM`: `HAUMDAUCHER Identity <service@haumdaucher.de>`
|
||||
|
||||
---
|
||||
|
||||
### 2. Customizing Email Stages in Authentik
|
||||
Authentik controls outgoing emails via **Email Stages**:
|
||||
1. Open Admin UI: **Flows & Stages > Stages**.
|
||||
2. Locate the relevant email stage:
|
||||
* `default-password-reset-email` (for password recovery & manual user activation)
|
||||
* `default-enrollment-email` / `invitation-email` (for new user invitations)
|
||||
* `default-user-settings-password-reset-email`
|
||||
3. Click **Edit** on the stage:
|
||||
* **Subject**: `Willkommen bei Haumdaucher e.V. – Aktiviere deinen Zugang`
|
||||
* **Token Expiry**: `Minutes=1440` (24 hours) or `Minutes=10080` (7 days)
|
||||
* **Template**: Select standard template or your custom template path.
|
||||
|
||||
---
|
||||
|
||||
### 3. Customizing the HTML Email Template (Django Syntax)
|
||||
Authentik email templates use Django template syntax. Available variables in email templates include:
|
||||
* `{{ user.name }}` / `{{ user.username }}` / `{{ user.email }}`
|
||||
* `{{ user.attributes.haumdaucher_mitgliedsnummer }}`
|
||||
* `{{ url }}` (The activation / password reset link)
|
||||
* `{{ token_expires }}` (Expiry timestamp)
|
||||
* `{{ brand.branding_title }}` (`HAUMDAUCHER`)
|
||||
|
||||
#### Example Custom Haumdaucher Welcome Template
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { font-family: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: #0d0d12; color: #f0f0f5; margin: 0; padding: 40px; }
|
||||
.card { background: #181824; border: 1px solid rgba(212, 175, 55, 0.3); border-radius: 16px; max-width: 540px; margin: 0 auto; padding: 32px; box-shadow: 0 12px 30px rgba(0,0,0,0.5); }
|
||||
.header { text-align: center; margin-bottom: 24px; }
|
||||
.logo { height: 60px; }
|
||||
h1 { color: #d4af37; font-size: 22px; margin-top: 12px; }
|
||||
p { color: #c0c0d0; line-height: 1.6; font-size: 15px; }
|
||||
.btn { display: block; text-align: center; background: linear-gradient(135deg, #d4af37, #aa8520); color: #000; font-weight: bold; text-decoration: none; padding: 14px 24px; border-radius: 10px; margin: 28px 0; }
|
||||
.meta { background: rgba(255,255,255,0.05); border-radius: 8px; padding: 12px 16px; font-size: 13px; color: #a0a0b0; }
|
||||
.footer { text-align: center; font-size: 12px; color: #606070; margin-top: 30px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="header">
|
||||
<img class="logo" src="https://haumdaucher.de/images/logo_classic.png" alt="Haumdaucher Logo" />
|
||||
<h1>Willkommen beim Haumdaucher e.V.</h1>
|
||||
</div>
|
||||
|
||||
<p>Hallo <strong>{{ user.name }}</strong>,</p>
|
||||
<p>dein zentraler Zugang für die Haumdaucher Cloud & Services wurde eingerichtet.</p>
|
||||
|
||||
{% if user.attributes.haumdaucher_mitgliedsnummer %}
|
||||
<div class="meta">
|
||||
<strong>Mitgliedsnummer:</strong> {{ user.attributes.haumdaucher_mitgliedsnummer }}<br/>
|
||||
<strong>Benutzername:</strong> {{ user.username }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p>Klicke auf den Button unten, um dein Passwort festzulegen und deinen Zugang zu aktivieren:</p>
|
||||
|
||||
<a class="btn" href="{{ url }}">Zugang aktivieren & Passwort setzen</a>
|
||||
|
||||
<p style="font-size: 12px; color: #888;">Dieser Link ist 7 Tage lang gültig. Falls du die Registrierung nicht angefordert hast, kannst du diese E-Mail ignorieren.</p>
|
||||
|
||||
<div class="footer">
|
||||
HAUMDAUCHER e.V. • <a href="https://haumdaucher.de" style="color: #d4af37;">haumdaucher.de</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Access Control & Authorization Policies (Limiting App Access)
|
||||
|
||||
By default, an application without an attached policy allows **any authenticated user** to access it. Authentik provides fine-grained Access Control (RBAC & PBAC) to restrict access per application.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "Authentik Directory Groups"
|
||||
Admin["👑 <b>infra-admins</b><br/>(Moritz & Wife / Full Admin)"]
|
||||
Verein["🍺 <b>verein-mitglieder</b><br/>(Club Members)"]
|
||||
Normal["👤 <b>users</b><br/>(Normal Authenticated Users)"]
|
||||
end
|
||||
|
||||
subgraph "Tier 1: Admin & Ops Tools (ForwardAuth / OIDC)"
|
||||
Z2M["Zigbee2MQTT"]
|
||||
Prom["Prometheus & Alertmanager"]
|
||||
PMA["phpMyAdmin (Nextcloud & Gitea)"]
|
||||
N8N["n8n Automation"]
|
||||
MinIO["MinIO Console"]
|
||||
Coder["Coder v2"]
|
||||
OpenClaw["OpenClaw Engine"]
|
||||
end
|
||||
|
||||
subgraph "Tier 2: Daily Shared Services (OIDC)"
|
||||
HASS["Home Assistant"]
|
||||
Grafana["Grafana Dashboards"]
|
||||
Nextcloud["Nextcloud Cloud"]
|
||||
Gitea["Gitea Git Platform"]
|
||||
ClubWeb["haumdaucher.de"]
|
||||
end
|
||||
|
||||
%% Admin Links (Full Superadmin access everywhere)
|
||||
Admin ==>|Full Access| Z2M
|
||||
Admin ==>|Full Access| OpenClaw
|
||||
Admin ==>|Admin| HASS
|
||||
Admin ==>|Admin| Grafana
|
||||
Admin ==>|Admin| Nextcloud
|
||||
Admin ==>|Admin| Gitea
|
||||
Admin ==>|Admin| ClubWeb
|
||||
|
||||
%% Normal User Links (Restricted)
|
||||
Normal -->|Viewer (Read-Only)| Grafana
|
||||
Normal -->|Dashboard View| HASS
|
||||
Normal -->|Standard User| Nextcloud
|
||||
Normal -->|Developer| Gitea
|
||||
|
||||
%% Club Members Links
|
||||
Verein -->|Member Portal| ClubWeb
|
||||
Verein -->|Club Shared Folder| Nextcloud
|
||||
Verein -->|Viewer (Read-Only)| Grafana
|
||||
```
|
||||
|
||||
### Master Permission Matrix
|
||||
|
||||
| Application | Domain | Auth Type | `infra-admins` (Moritz & Wife) | `verein-mitglieder` (Club) | `users` (Normal Users) |
|
||||
| :--- | :--- | :--- | :---: | :---: | :---: |
|
||||
| **OpenClaw** | `openclaw.haumdaucher.de` | ForwardAuth | **Full Access** ✅ | ❌ *Denied* | ❌ *Denied* |
|
||||
| **Zigbee2MQTT** | `zigbee2mqtt.moritzgraf.de` | ForwardAuth | **Full Access** ✅ | ❌ *Denied* | ❌ *Denied* |
|
||||
| **Prometheus / Alertmanager** | `prometheus.haumdaucher.de` | ForwardAuth | **Full Access** ✅ | ❌ *Denied* | ❌ *Denied* |
|
||||
| **phpMyAdmin (All DBs)** | `*.phpmyadmin.haumdaucher.de` | ForwardAuth | **Full Access** ✅ | ❌ *Denied* | ❌ *Denied* |
|
||||
| **n8n** | `n8n.moritzgraf.de` | ForwardAuth | **Full Access** ✅ | ❌ *Denied* | ❌ *Denied* |
|
||||
| **MinIO Console** | `minio-console.haumdaucher.de` | Native OIDC | **`consoleAdmin`** ✅ | ❌ *Denied* | ❌ *Denied* |
|
||||
| **Coder v2** | `coder.moritzgraf.de` | Native OIDC | **`Owner / Admin`** ✅ | ❌ *Denied* | ❌ *Denied* |
|
||||
| **Home Assistant** | `hass.moritzgraf.de` | Native OIDC | **`Administrator`** ✅ | ❌ *Denied* | **`Standard User`** |
|
||||
| **Grafana** | `grafana.haumdaucher.de` | Native OIDC | **`Admin`** ✅ | **`Viewer` (Read-Only)** | **`Viewer` (Read-Only)** |
|
||||
| **Nextcloud** | `cloud.haumdaucher.de` | Native OIDC | **`Admin`** ✅ | **`User` (Club Folder)** | **`User` (Standard)** |
|
||||
| **Gitea** | `git.moritzgraf.de` | Native OIDC | **`Admin`** ✅ | ❌ / Member | **`Developer`** |
|
||||
| **haumdaucher.de** | `haumdaucher.de` | ForwardAuth / OIDC | **`Admin`** ✅ | **`Member Access`** ✅ | ❌ *Denied* |
|
||||
|
||||
### Policy Bindings in Terraform
|
||||
All policy bindings are enforced directly in `terraform/authentik.tf`:
|
||||
|
||||
```hcl
|
||||
# 1. Admin-Only Tools (Restricted strictly to infra-admins)
|
||||
resource "authentik_policy_binding" "openclaw_admin_only" {
|
||||
target = authentik_application.openclaw.uuid
|
||||
group = authentik_group.infra_admins.id
|
||||
order = 0
|
||||
}
|
||||
|
||||
resource "authentik_policy_binding" "zigbee2mqtt_admin_only" {
|
||||
target = authentik_application.zigbee2mqtt.uuid
|
||||
group = authentik_group.infra_admins.id
|
||||
order = 0
|
||||
}
|
||||
|
||||
# 2. Shared & Member Applications (Accessible to users group)
|
||||
resource "authentik_policy_binding" "grafana_users" {
|
||||
target = authentik_application.grafana.uuid
|
||||
group = authentik_group.users.id
|
||||
order = 0
|
||||
}
|
||||
|
||||
resource "authentik_policy_binding" "nextcloud_users" {
|
||||
target = authentik_application.nextcloud.uuid
|
||||
group = authentik_group.users.id
|
||||
order = 0
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Attribute-Based Policy (Python Expression Policy)
|
||||
You can write Python expressions that evaluate custom user attributes dynamically:
|
||||
|
||||
```hcl
|
||||
resource "authentik_policy_expression" "require_active_membership" {
|
||||
name = "require-active-haumdaucher-membership"
|
||||
expression = <<EOF
|
||||
# Must have a valid Mitgliedsnummer and active status
|
||||
mitgliedsnummer = request.user.attributes.get('haumdaucher_mitgliedsnummer')
|
||||
status = request.user.attributes.get('status', 'aktiv')
|
||||
|
||||
if not mitgliedsnummer or not mitgliedsnummer.startswith('HM-'):
|
||||
ak_message("Keine gültige Haumdaucher Mitgliedsnummer hinterlegt.")
|
||||
return False
|
||||
|
||||
if status != 'aktiv':
|
||||
ak_message("Deine Mitgliedschaft ist derzeit nicht aktiv.")
|
||||
return False
|
||||
|
||||
return True
|
||||
EOF
|
||||
}
|
||||
|
||||
# Attach policy to Nextcloud application
|
||||
resource "authentik_policy_binding" "nextcloud_active_members" {
|
||||
target = authentik_application.nextcloud.uuid
|
||||
policy = authentik_policy_expression.require_active_membership.id
|
||||
order = 1
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏷️ Custom Attributes & OIDC Claim Mappings
|
||||
|
||||
Authentik allows storing any arbitrary JSON object inside `user.attributes`.
|
||||
|
||||
### 1. Recommended Attribute Schema for Haumdaucher
|
||||
```json
|
||||
{
|
||||
"haumdaucher_mitgliedsnummer": "HM-2024-042",
|
||||
"second_email": "max.privat@gmail.com",
|
||||
"eintrittsdatum": "2024-01-15",
|
||||
"verein_rolle": "Vorstand / Kassenwart / Mitglied",
|
||||
"phone": "+49 170 1234567",
|
||||
"telegram_handle": "@max_mustermann"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Exposing Custom Attributes via OIDC Claims (Terraform)
|
||||
To pass `haumdaucher_mitgliedsnummer` or `second_email` to downstream applications like **Nextcloud**, **Grafana**, or **Gitea**:
|
||||
|
||||
```hcl
|
||||
# Define a custom Scope Mapping in Terraform
|
||||
resource "authentik_property_mapping_provider_scope" "haumdaucher_profile" {
|
||||
name = "Haumdaucher Extended Profile"
|
||||
scope_name = "haumdaucher_profile"
|
||||
description = "Provides Vereins-Mitgliedsnummer and secondary email to OIDC clients"
|
||||
expression = <<EOF
|
||||
return {
|
||||
"haumdaucher_mitgliedsnummer": request.user.attributes.get("haumdaucher_mitgliedsnummer", ""),
|
||||
"second_email": request.user.attributes.get("second_email", ""),
|
||||
"verein_rolle": request.user.attributes.get("verein_rolle", "Mitglied")
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Add this mapping to the OAuth2 provider (e.g. Nextcloud or Grafana)
|
||||
resource "authentik_provider_oauth2" "nextcloud" {
|
||||
name = "Nextcloud"
|
||||
client_id = "nextcloud"
|
||||
client_secret = random_password.nextcloud_oauth_client_secret.result
|
||||
client_type = "confidential"
|
||||
authorization_flow = data.authentik_flow.default_authorization_flow.id
|
||||
invalidation_flow = data.authentik_flow.default_invalidation_flow.id
|
||||
signing_key = data.authentik_certificate_key_pair.generated.id
|
||||
|
||||
property_mappings = [
|
||||
data.authentik_property_mapping_provider_scope.openid.id,
|
||||
data.authentik_property_mapping_provider_scope.email.id,
|
||||
data.authentik_property_mapping_provider_scope.profile.id,
|
||||
authentik_property_mapping_provider_scope.haumdaucher_profile.id, # <-- Custom scope
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Production Checklist & Best Practices
|
||||
|
||||
When managing users in production, keep the following considerations in mind:
|
||||
|
||||
### 1. Passwordless & Multi-Factor Authentication (MFA / 2FA)
|
||||
* Enforce **WebAuthn / Passkeys (FIDO2)** or **TOTP (Authenticator App)** for users in the `infra-admins` group.
|
||||
* In Authentik, this is done by binding an Authenticator Validation Stage to the default authentication flow or creating an Admin Authorization Flow.
|
||||
|
||||
### 2. User Self-Service & Attribute Editing
|
||||
* Authentik provides a self-service portal at `https://auth.haumdaucher.de/if/user/#/settings`.
|
||||
* Users can change their password, register Passkeys, and edit user profile fields.
|
||||
* **Security Rule**: Keep administrative attributes (like `haumdaucher_mitgliedsnummer`) read-only by not exposing them to the user edit flow, while allowing users to freely edit personal contact details like `second_email` or `phone`.
|
||||
|
||||
### 3. Offboarding & Member Deactivation
|
||||
* If a member leaves the club:
|
||||
1. Set `is_active = false` (or remove from `verein-mitglieder` group).
|
||||
2. Authentik immediately terminates active sessions and revokes ForwardAuth access and OIDC refresh tokens.
|
||||
3. No need to manually reset passwords on Nextcloud, Grafana, or Gitea—SSO handles deprovisioning instantly.
|
||||
|
||||
### 4. GDPR / DSGVO & Privacy
|
||||
* Store only necessary data in user attributes.
|
||||
* If a member requests account deletion, deleting the user in Authentik cascades identity termination across all connected services.
|
||||
|
||||
### 5. Backups & Audit Logs
|
||||
* User database changes are stored in the PostgreSQL cluster (`authentik-db`) and backed up with daily Velero snapshot schedules.
|
||||
* Authentik logs all login attempts, failed logins, and administrative modifications under **Events > Logs**.
|
||||
@@ -0,0 +1,24 @@
|
||||
apiVersion: postgresql.cnpg.io/v1
|
||||
kind: Cluster
|
||||
metadata:
|
||||
name: authentik-db
|
||||
namespace: auth
|
||||
spec:
|
||||
instances: 1
|
||||
bootstrap:
|
||||
initdb:
|
||||
database: authentik
|
||||
owner: authentik
|
||||
postgresql:
|
||||
parameters:
|
||||
shared_buffers: "128MB"
|
||||
max_connections: "100"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
memory: 2Gi
|
||||
storage:
|
||||
storageClass: openebs-hostpath
|
||||
size: 5Gi
|
||||
@@ -0,0 +1,62 @@
|
||||
# --- Authentik Helm Configuration ---
|
||||
# Uses CloudNative-PG cluster (authentik-db) for PostgreSQL
|
||||
|
||||
global:
|
||||
env:
|
||||
- name: AUTHENTIK_POSTGRESQL__HOST
|
||||
value: "authentik-db-rw.auth.svc.cluster.local"
|
||||
- name: AUTHENTIK_POSTGRESQL__NAME
|
||||
value: "authentik"
|
||||
- name: AUTHENTIK_POSTGRESQL__USER
|
||||
value: "authentik"
|
||||
- name: AUTHENTIK_POSTGRESQL__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: authentik-db-app
|
||||
key: password
|
||||
|
||||
authentik:
|
||||
# Secret key for cookie signing and session encryption
|
||||
secret_key: "gU2ohe8eighaiph4quaeCh1ieM3iechoh2ohNg5yahz6eephoh"
|
||||
error_reporting:
|
||||
enabled: false
|
||||
log_level: info
|
||||
|
||||
server:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 384Mi
|
||||
limits:
|
||||
memory: 768Mi
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
ingressClassName: "traefik"
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: "traefik"
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
kubernetes.io/tls-acme: "true"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||
hosts:
|
||||
- auth.haumdaucher.de
|
||||
tls:
|
||||
- hosts:
|
||||
- auth.haumdaucher.de
|
||||
secretName: auth-haumdaucher-de
|
||||
|
||||
worker:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 384Mi
|
||||
limits:
|
||||
memory: 768Mi
|
||||
|
||||
# Disable bundled subchart databases (managed by CNPG)
|
||||
postgresql:
|
||||
enabled: false
|
||||
@@ -1,25 +0,0 @@
|
||||
# # status: removed again
|
||||
|
||||
# apiVersion: helm.fluxcd.io/v1
|
||||
# kind: HelmRelease
|
||||
# metadata:
|
||||
# name: helm-phpldapadmin
|
||||
# namespace: flux
|
||||
# spec:
|
||||
# releaseName: phpldapadmin
|
||||
# targetNamespace: auth
|
||||
# chart:
|
||||
# repository: https://cetic.github.io/helm-charts
|
||||
# version: 0.1.3
|
||||
# name: phpldapadmin
|
||||
# values:
|
||||
# service:
|
||||
# type: ClusterIP
|
||||
# ingress:
|
||||
# enabled: true
|
||||
# hosts:
|
||||
# - "ldap.haumdaucher.de"
|
||||
# annotations:
|
||||
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
# tls:
|
||||
# - ldap-haumdaucher-de
|
||||
@@ -0,0 +1,26 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: authentik-forwardauth
|
||||
namespace: auth
|
||||
spec:
|
||||
forwardAuth:
|
||||
address: http://authentik-server.auth.svc.cluster.local/outpost.goauthentik.io/auth/traefik
|
||||
trustForwardHeader: true
|
||||
authRequestHeaders:
|
||||
- Authorization
|
||||
- Cookie
|
||||
- Accept
|
||||
authResponseHeaders:
|
||||
- X-authentik-username
|
||||
- X-authentik-groups
|
||||
- X-authentik-email
|
||||
- X-authentik-name
|
||||
- X-authentik-uid
|
||||
- X-authentik-jwt
|
||||
- X-authentik-meta-jwks
|
||||
- X-authentik-meta-outpost
|
||||
- X-authentik-meta-provider
|
||||
- X-authentik-meta-app
|
||||
- X-authentik-meta-version
|
||||
- authorization
|
||||
Binary file not shown.
@@ -1,13 +1,74 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: gitea-db-adminer
|
||||
namespace: development
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: gitea-db-adminer
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: gitea-db-adminer
|
||||
spec:
|
||||
containers:
|
||||
- name: adminer
|
||||
image: adminer:latest
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
env:
|
||||
- name: ADMINER_DEFAULT_SERVER
|
||||
value: "gitea-postgresql"
|
||||
- name: ADMINER_DESIGN
|
||||
value: "dracula"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
memory: 128Mi
|
||||
---
|
||||
db:
|
||||
host: gitea-mariadb
|
||||
ingress:
|
||||
enabled: true
|
||||
hosts:
|
||||
- path: "/"
|
||||
tls: true
|
||||
name: "gitea.phpmyadmin.haumdaucher.de"
|
||||
tlsSecret: "gitea-phpmyadmin-haumdaucher-de"
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: gitea-db-adminer
|
||||
namespace: development
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
name: http
|
||||
selector:
|
||||
app: gitea-db-adminer
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: gitea-phpmyadmin
|
||||
namespace: development
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||
kubernetes.io/ingress.class: "traefik"
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
traefik.ingress.kubernetes.io/router.middlewares: auth-authentik-forwardauth@kubernetescrd
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
- hosts:
|
||||
- gitea.phpmyadmin.haumdaucher.de
|
||||
secretName: gitea-phpmyadmin-haumdaucher-de
|
||||
rules:
|
||||
- host: gitea.phpmyadmin.haumdaucher.de
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: gitea-db-adminer
|
||||
port:
|
||||
number: 80
|
||||
@@ -0,0 +1,65 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: grabengasse-backend
|
||||
namespace: grabengasse
|
||||
labels:
|
||||
app: grabengasse-backend
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: grabengasse-backend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: grabengasse-backend
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: registry-haumdaucher-de
|
||||
containers:
|
||||
- name: backend
|
||||
image: registry.haumdaucher.de/website/grabengasse_backend:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8000
|
||||
name: http
|
||||
env:
|
||||
- name: PROMETHEUS_URL
|
||||
value: "http://prometheus-operator-kube-p-prometheus.monitoring.svc.cluster.local:9090"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 15
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: grabengasse-backend
|
||||
namespace: grabengasse
|
||||
labels:
|
||||
app: grabengasse-backend
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 8000
|
||||
targetPort: 8000
|
||||
name: http
|
||||
selector:
|
||||
app: grabengasse-backend
|
||||
@@ -0,0 +1,62 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: grabengasse-frontend
|
||||
namespace: grabengasse
|
||||
labels:
|
||||
app: grabengasse-frontend
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: grabengasse-frontend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: grabengasse-frontend
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: registry-haumdaucher-de
|
||||
containers:
|
||||
- name: frontend
|
||||
image: registry.haumdaucher.de/website/grabengasse_frontend:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
resources:
|
||||
requests:
|
||||
cpu: 25m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 80
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 80
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 15
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: grabengasse-frontend
|
||||
namespace: grabengasse
|
||||
labels:
|
||||
app: grabengasse-frontend
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
name: http
|
||||
selector:
|
||||
app: grabengasse-frontend
|
||||
@@ -0,0 +1,35 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: grabengasse7
|
||||
namespace: grabengasse
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: "traefik"
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
kubernetes.io/tls-acme: "true"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
- hosts:
|
||||
- grabengasse7.de
|
||||
secretName: grabengasse7-de
|
||||
rules:
|
||||
- host: grabengasse7.de
|
||||
http:
|
||||
paths:
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: grabengasse-backend
|
||||
port:
|
||||
number: 8000
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: grabengasse-frontend
|
||||
port:
|
||||
number: 80
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: grabengasse
|
||||
labels:
|
||||
name: grabengasse
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -15,7 +15,7 @@ data:
|
||||
"tags": ["home", "weather", "outdoor", "prometheus"],
|
||||
"timezone": "browser",
|
||||
"schemaVersion": 39,
|
||||
"version": 3,
|
||||
"version": 4,
|
||||
"refresh": "30s",
|
||||
"editable": false,
|
||||
"graphTooltip": 2,
|
||||
@@ -29,7 +29,7 @@ data:
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"targets": [
|
||||
{
|
||||
"expr": "homeassistant_sensor_unit_lqi{entity=\"sensor.shelly_ws90_linkquality\"}",
|
||||
"expr": "homeassistant_sensor_unit_lqi{entity=\"sensor.0xfc4d6afffe246794_linkquality\"}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
@@ -65,7 +65,7 @@ data:
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"targets": [
|
||||
{
|
||||
"expr": "homeassistant_sensor_battery_percent{entity=\"sensor.shelly_ws90_batterie\"}",
|
||||
"expr": "homeassistant_sensor_battery_percent{entity=\"sensor.shelly_ws90_battery\"}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
@@ -100,7 +100,7 @@ data:
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"targets": [
|
||||
{
|
||||
"expr": "homeassistant_sensor_atmospheric_pressure_hpa{entity=\"sensor.shelly_ws90_atmospharischer_druck\"}",
|
||||
"expr": "homeassistant_sensor_atmospheric_pressure_hpa{entity=\"sensor.shelly_ws90_pressure\"}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
@@ -166,7 +166,7 @@ data:
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"targets": [
|
||||
{
|
||||
"expr": "homeassistant_sensor_temperature_celsius{entity=\"sensor.shelly_ws90_temperatur\"}",
|
||||
"expr": "homeassistant_sensor_temperature_celsius{entity=\"sensor.shelly_ws90_temperature\"}",
|
||||
"legendFormat": "Temperature",
|
||||
"refId": "A"
|
||||
},
|
||||
@@ -191,7 +191,7 @@ data:
|
||||
"refId": "E"
|
||||
},
|
||||
{
|
||||
"expr": "homeassistant_sensor_humidity_percent{entity=\"sensor.shelly_ws90_luftfeuchtigkeit\"}",
|
||||
"expr": "homeassistant_sensor_humidity_percent{entity=\"sensor.shelly_ws90_humidity\"}",
|
||||
"legendFormat": "Relative Humidity",
|
||||
"refId": "F"
|
||||
},
|
||||
@@ -472,7 +472,7 @@ data:
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"targets": [
|
||||
{
|
||||
"expr": "homeassistant_sensor_atmospheric_pressure_hpa{entity=\"sensor.shelly_ws90_atmospharischer_druck\"}",
|
||||
"expr": "homeassistant_sensor_atmospheric_pressure_hpa{entity=\"sensor.shelly_ws90_pressure\"}",
|
||||
"legendFormat": "Pressure",
|
||||
"refId": "A"
|
||||
},
|
||||
|
||||
@@ -12,10 +12,10 @@ data:
|
||||
{
|
||||
"uid": "taupi-fan",
|
||||
"title": "💨 Cellar Fan (TAUPI)",
|
||||
"tags": ["climate", "fan", "cellar", "prometheus"],
|
||||
"tags": ["climate", "fan", "cellar", "taupi", "prometheus"],
|
||||
"timezone": "browser",
|
||||
"schemaVersion": 39,
|
||||
"version": 3,
|
||||
"schemaVersion": 41,
|
||||
"version": 5,
|
||||
"refresh": "30s",
|
||||
"editable": false,
|
||||
"graphTooltip": 2,
|
||||
@@ -23,22 +23,39 @@ data:
|
||||
"from": "now-24h",
|
||||
"to": "now"
|
||||
},
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"name": "datasource",
|
||||
"type": "datasource",
|
||||
"pluginId": "prometheus",
|
||||
"includeAll": false,
|
||||
"label": "Prometheus",
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": "prometheus",
|
||||
"value": "prometheus"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"panels": [
|
||||
{
|
||||
"title": "⚡ Status & Controls",
|
||||
"title": "⚡ Status & Real-Time KPIs",
|
||||
"type": "row",
|
||||
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 0},
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Fan Power Relay",
|
||||
"type": "stat",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 4, "w": 6, "x": 0, "y": 1},
|
||||
"datasource": {"type": "prometheus", "uid": "${datasource}"},
|
||||
"gridPos": {"h": 4, "w": 4, "x": 0, "y": 1},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"mappings": [
|
||||
{"type": "value", "options": {"0": {"text": "OFF", "color": "blue"}, "1": {"text": "ON", "color": "orange"}}}
|
||||
{"type": "value", "options": {"0": {"text": "OFF (Idle)", "color": "blue"}, "1": {"text": "VENTILATING", "color": "orange"}}}
|
||||
],
|
||||
"color": {"mode": "thresholds"}
|
||||
}
|
||||
@@ -50,18 +67,86 @@ data:
|
||||
"reduceOptions": {"values": false, "calcs": ["lastNotNull"]}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_relay_status", "legendFormat": "Fan Status", "refId": "A"}
|
||||
{"expr": "taupi_relay_status", "legendFormat": "Relay", "refId": "A"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Mold Danger Status",
|
||||
"id": 2,
|
||||
"title": "Drying Quality Score",
|
||||
"description": "Calculated 0-100 score based on Dewpoint Spread. Higher score allows longer continuous ventilation runtime.",
|
||||
"type": "stat",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 4, "w": 6, "x": 6, "y": 1},
|
||||
"datasource": {"type": "prometheus", "uid": "${datasource}"},
|
||||
"gridPos": {"h": 4, "w": 4, "x": 4, "y": 1},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "none",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"color": {"mode": "thresholds"},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{"color": "gray", "value": null},
|
||||
{"color": "yellow", "value": 40},
|
||||
{"color": "green", "value": 70},
|
||||
{"color": "purple", "value": 90}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"graphMode": "none",
|
||||
"colorMode": "background",
|
||||
"textMode": "auto",
|
||||
"reduceOptions": {"values": false, "calcs": ["lastNotNull"]}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_quality_score", "legendFormat": "Quality Score", "refId": "A"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Dewpoint Drying Spread",
|
||||
"description": "Indoor Dewpoint − Outdoor Dewpoint. Must be > 2.0°C for drying.",
|
||||
"type": "stat",
|
||||
"datasource": {"type": "prometheus", "uid": "${datasource}"},
|
||||
"gridPos": {"h": 4, "w": 4, "x": 8, "y": 1},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "celsius",
|
||||
"decimals": 1,
|
||||
"color": {"mode": "thresholds"},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{"color": "red", "value": null},
|
||||
{"color": "yellow", "value": 0},
|
||||
{"color": "green", "value": 2.0}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"graphMode": "none",
|
||||
"colorMode": "background",
|
||||
"textMode": "auto",
|
||||
"reduceOptions": {"values": false, "calcs": ["lastNotNull"]}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_dewpoint_spread_celsius", "legendFormat": "Dewpoint Spread", "refId": "A"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Thermal Governor State",
|
||||
"description": "Thermal operating regime (Warm Cooling, Comfort 14-20°C, Cold Restricted, Frost Floor)",
|
||||
"type": "stat",
|
||||
"datasource": {"type": "prometheus", "uid": "${datasource}"},
|
||||
"gridPos": {"h": 4, "w": 4, "x": 12, "y": 1},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"mappings": [
|
||||
{"type": "value", "options": {"0": {"text": "SAFE", "color": "green"}, "1": {"text": "CRITICAL", "color": "red"}}}
|
||||
{"type": "value", "options": {"0": {"text": "INIT", "color": "gray"}, "1": {"text": "FROST FLOOR", "color": "blue"}, "2": {"text": "COMFORT (14-20°)", "color": "green"}, "3": {"text": "WARM (Cooling)", "color": "orange"}, "4": {"text": "COLD RESTRICTED", "color": "purple"}}}
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -72,19 +157,30 @@ data:
|
||||
"reduceOptions": {"values": false, "calcs": ["lastNotNull"]}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_is_critical", "legendFormat": "Mold Danger", "refId": "A"}
|
||||
{"expr": "taupi_thermal_governor_state", "legendFormat": "Governor", "refId": "A"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Activation Criteria Met",
|
||||
"id": 5,
|
||||
"title": "Dynamic Max Burst Runtime",
|
||||
"description": "Dynamic max continuous runtime allowed based on drying quality",
|
||||
"type": "stat",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 4, "w": 6, "x": 12, "y": 1},
|
||||
"datasource": {"type": "prometheus", "uid": "${datasource}"},
|
||||
"gridPos": {"h": 4, "w": 4, "x": 16, "y": 1},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"mappings": [
|
||||
{"type": "value", "options": {"0": {"text": "NO", "color": "gray"}, "1": {"text": "YES", "color": "green"}}}
|
||||
]
|
||||
"unit": "m",
|
||||
"decimals": 0,
|
||||
"color": {"mode": "thresholds"},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{"color": "gray", "value": null},
|
||||
{"color": "yellow", "value": 20},
|
||||
{"color": "green", "value": 45},
|
||||
{"color": "purple", "value": 90}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@@ -94,14 +190,15 @@ data:
|
||||
"reduceOptions": {"values": false, "calcs": ["lastNotNull"]}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_would_fan_activate", "legendFormat": "Dewpoint Condition", "refId": "A"}
|
||||
{"expr": "taupi_max_allowed_runtime_minutes", "legendFormat": "Max Runtime", "refId": "A"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Shelly Plug Reachability",
|
||||
"id": 6,
|
||||
"title": "Controller Connectivity",
|
||||
"type": "stat",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 4, "w": 6, "x": 18, "y": 1},
|
||||
"datasource": {"type": "prometheus", "uid": "${datasource}"},
|
||||
"gridPos": {"h": 4, "w": 4, "x": 20, "y": 1},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"mappings": [
|
||||
@@ -116,23 +213,113 @@ data:
|
||||
"reduceOptions": {"values": false, "calcs": ["lastNotNull"]}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "up{job=\"taupi-fan\"}", "legendFormat": "Connectivity", "refId": "A"}
|
||||
{"expr": "up{job=\"taupi-fan\"}", "legendFormat": "Shelly Scrape", "refId": "A"}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"title": "📊 Climate Correlation",
|
||||
"title": "🎯 PRIMARY GOAL: Humidity Reduction vs. Fan Operation",
|
||||
"type": "row",
|
||||
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 5},
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"title": "🎯 Primary Goal: Humidity High ➔ Fan Runs ➔ Humidity Drops",
|
||||
"description": "Visual validation of cellar drying: When Indoor Humidity rises above target, the orange Fan Active band engages until Indoor Humidity drops back down.",
|
||||
"type": "timeseries",
|
||||
"datasource": {"type": "prometheus", "uid": "${datasource}"},
|
||||
"gridPos": {"h": 12, "w": 24, "x": 0, "y": 6},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "percent",
|
||||
"custom": {
|
||||
"lineWidth": 2,
|
||||
"fillOpacity": 0,
|
||||
"spanNulls": true,
|
||||
"showPoints": "never"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Fan Active (Running)"},
|
||||
"properties": [
|
||||
{"id": "color", "value": {"fixedColor": "#FF9800", "mode": "fixed"}},
|
||||
{"id": "custom.axisPlacement", "value": "hidden"},
|
||||
{"id": "custom.drawStyle", "value": "line"},
|
||||
{"id": "custom.lineInterpolation", "value": "stepAfter"},
|
||||
{"id": "custom.lineWidth", "value": 0},
|
||||
{"id": "custom.fillOpacity", "value": 25},
|
||||
{"id": "min", "value": 0},
|
||||
{"id": "max", "value": 1},
|
||||
{"id": "decimals", "value": 0},
|
||||
{"id": "mappings", "value": [{"type": "value", "options": {"0": {"text": "OFF"}, "1": {"text": "ON"}}}]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Indoor Humidity (Cellar)"},
|
||||
"properties": [
|
||||
{"id": "custom.axisPlacement", "value": "left"},
|
||||
{"id": "color", "value": {"fixedColor": "#00E5FF", "mode": "fixed"}},
|
||||
{"id": "custom.lineWidth", "value": 3},
|
||||
{"id": "custom.fillOpacity", "value": 12}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Target Setpoint"},
|
||||
"properties": [
|
||||
{"id": "custom.axisPlacement", "value": "left"},
|
||||
{"id": "color", "value": {"fixedColor": "#E040FB", "mode": "fixed"}},
|
||||
{"id": "custom.lineStyle", "value": {"fill": "dash", "dash": [6, 4]}},
|
||||
{"id": "custom.lineWidth", "value": 2}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Mold Risk Limit (Sedlbauer)"},
|
||||
"properties": [
|
||||
{"id": "custom.axisPlacement", "value": "left"},
|
||||
{"id": "color", "value": {"fixedColor": "#FF1744", "mode": "fixed"}},
|
||||
{"id": "custom.lineStyle", "value": {"fill": "dash", "dash": [10, 5]}},
|
||||
{"id": "custom.lineWidth", "value": 1.5}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Outdoor Humidity"},
|
||||
"properties": [
|
||||
{"id": "custom.axisPlacement", "value": "left"},
|
||||
{"id": "color", "value": {"fixedColor": "#448AFF", "mode": "fixed"}},
|
||||
{"id": "custom.lineWidth", "value": 1.5},
|
||||
{"id": "custom.lineStyle", "value": {"fill": "dot", "dash": [2, 4]}}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull", "min", "max", "mean"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_relay_status", "legendFormat": "Fan Active (Running)", "refId": "A"},
|
||||
{"expr": "taupi_humidity_percent_innen", "legendFormat": "Indoor Humidity (Cellar)", "refId": "B"},
|
||||
{"expr": "taupi_target_humidity_percent", "legendFormat": "Target Setpoint", "refId": "C"},
|
||||
{"expr": "taupi_critical_humidity_threshold_percent", "legendFormat": "Mold Risk Limit (Sedlbauer)", "refId": "D"},
|
||||
{"expr": "taupi_humidity_percent_aussen", "legendFormat": "Outdoor Humidity", "refId": "E"}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"title": "📊 Full Climate & Fan Correlation",
|
||||
"description": "All metrics in one view. Left axis: temperatures & dewpoints (°C). Right axis: humidity & mold threshold (%). Colored background bands: orange = fan running, red = mold critical, green = activation criteria met. Hover anywhere to read all values at that timestamp.",
|
||||
"title": "📊 Complete Climate & Psychrometric Correlation",
|
||||
"type": "row",
|
||||
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 18},
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"title": "📊 Full Telemetry Overview (Temperatures, Dewpoints, Humidity & Fan)",
|
||||
"description": "Comprehensive correlation: Left axis (°C) = Temperatures & Dewpoints. Right axis (%) = Humidity, Setpoint & Mold Limit. Orange shaded band = Fan Relay ON.",
|
||||
"type": "timeseries",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 14, "w": 24, "x": 0, "y": 6},
|
||||
"datasource": {"type": "prometheus", "uid": "${datasource}"},
|
||||
"gridPos": {"h": 13, "w": 24, "x": 0, "y": 19},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
@@ -151,43 +338,13 @@ data:
|
||||
{"id": "custom.drawStyle", "value": "line"},
|
||||
{"id": "custom.lineInterpolation", "value": "stepAfter"},
|
||||
{"id": "custom.lineWidth", "value": 0},
|
||||
{"id": "custom.fillOpacity", "value": 15},
|
||||
{"id": "custom.fillOpacity", "value": 20},
|
||||
{"id": "min", "value": 0},
|
||||
{"id": "max", "value": 1},
|
||||
{"id": "decimals", "value": 0},
|
||||
{"id": "mappings", "value": [{"type": "value", "options": {"0": {"text": "OFF"}, "1": {"text": "ON"}}}]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Mold Critical"},
|
||||
"properties": [
|
||||
{"id": "color", "value": {"fixedColor": "#F44336", "mode": "fixed"}},
|
||||
{"id": "custom.axisPlacement", "value": "hidden"},
|
||||
{"id": "custom.drawStyle", "value": "line"},
|
||||
{"id": "custom.lineInterpolation", "value": "stepAfter"},
|
||||
{"id": "custom.lineWidth", "value": 0},
|
||||
{"id": "custom.fillOpacity", "value": 10},
|
||||
{"id": "min", "value": 0},
|
||||
{"id": "max", "value": 1},
|
||||
{"id": "decimals", "value": 0},
|
||||
{"id": "mappings", "value": [{"type": "value", "options": {"0": {"text": "SAFE"}, "1": {"text": "CRITICAL"}}}]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Activation Criteria"},
|
||||
"properties": [
|
||||
{"id": "color", "value": {"fixedColor": "#4CAF50", "mode": "fixed"}},
|
||||
{"id": "custom.axisPlacement", "value": "hidden"},
|
||||
{"id": "custom.drawStyle", "value": "line"},
|
||||
{"id": "custom.lineInterpolation", "value": "stepAfter"},
|
||||
{"id": "custom.lineWidth", "value": 0},
|
||||
{"id": "custom.fillOpacity", "value": 8},
|
||||
{"id": "min", "value": 0},
|
||||
{"id": "max", "value": 1},
|
||||
{"id": "decimals", "value": 0},
|
||||
{"id": "mappings", "value": [{"type": "value", "options": {"0": {"text": "NO"}, "1": {"text": "YES"}}}]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Indoor Temp"},
|
||||
"properties": [
|
||||
@@ -211,9 +368,9 @@ data:
|
||||
"properties": [
|
||||
{"id": "custom.axisPlacement", "value": "left"},
|
||||
{"id": "unit", "value": "celsius"},
|
||||
{"id": "color", "value": {"fixedColor": "#FF6D00", "mode": "fixed"}},
|
||||
{"id": "custom.lineStyle", "value": {"fill": "dash", "dash": [10, 5]}},
|
||||
{"id": "custom.lineWidth", "value": 1}
|
||||
{"id": "color", "value": {"fixedColor": "#FFB74D", "mode": "fixed"}},
|
||||
{"id": "custom.lineStyle", "value": {"fill": "dash", "dash": [8, 4]}},
|
||||
{"id": "custom.lineWidth", "value": 1.5}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -221,9 +378,9 @@ data:
|
||||
"properties": [
|
||||
{"id": "custom.axisPlacement", "value": "left"},
|
||||
{"id": "unit", "value": "celsius"},
|
||||
{"id": "color", "value": {"fixedColor": "#2979FF", "mode": "fixed"}},
|
||||
{"id": "custom.lineStyle", "value": {"fill": "dash", "dash": [10, 5]}},
|
||||
{"id": "custom.lineWidth", "value": 1}
|
||||
{"id": "color", "value": {"fixedColor": "#90CAF9", "mode": "fixed"}},
|
||||
{"id": "custom.lineStyle", "value": {"fill": "dash", "dash": [8, 4]}},
|
||||
{"id": "custom.lineWidth", "value": 1.5}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -231,9 +388,8 @@ data:
|
||||
"properties": [
|
||||
{"id": "custom.axisPlacement", "value": "right"},
|
||||
{"id": "unit", "value": "percent"},
|
||||
{"id": "color", "value": {"fixedColor": "#00BFA5", "mode": "fixed"}},
|
||||
{"id": "custom.lineWidth", "value": 2},
|
||||
{"id": "custom.fillOpacity", "value": 5}
|
||||
{"id": "color", "value": {"fixedColor": "#00E5FF", "mode": "fixed"}},
|
||||
{"id": "custom.lineWidth", "value": 2.5}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -245,260 +401,203 @@ data:
|
||||
{"id": "custom.lineWidth", "value": 1}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Target Setpoint"},
|
||||
"properties": [
|
||||
{"id": "custom.axisPlacement", "value": "right"},
|
||||
{"id": "unit", "value": "percent"},
|
||||
{"id": "color", "value": {"fixedColor": "#E040FB", "mode": "fixed"}},
|
||||
{"id": "custom.lineStyle", "value": {"fill": "dash", "dash": [6, 4]}},
|
||||
{"id": "custom.lineWidth", "value": 1.5}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Mold Threshold"},
|
||||
"properties": [
|
||||
{"id": "custom.axisPlacement", "value": "right"},
|
||||
{"id": "unit", "value": "percent"},
|
||||
{"id": "color", "value": {"fixedColor": "#F44336", "mode": "fixed"}},
|
||||
{"id": "color", "value": {"fixedColor": "#FF1744", "mode": "fixed"}},
|
||||
{"id": "custom.lineStyle", "value": {"fill": "dash", "dash": [10, 5]}},
|
||||
{"id": "custom.lineWidth", "value": 1}
|
||||
{"id": "custom.lineWidth", "value": 1.5}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull", "min", "max"]},
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull", "min", "max", "mean"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_relay_status", "legendFormat": "Fan Active", "refId": "A"},
|
||||
{"expr": "taupi_is_critical", "legendFormat": "Mold Critical", "refId": "B"},
|
||||
{"expr": "taupi_would_fan_activate", "legendFormat": "Activation Criteria", "refId": "C"},
|
||||
{"expr": "taupi_temperature_celsius_innen", "legendFormat": "Indoor Temp", "refId": "D"},
|
||||
{"expr": "taupi_temperature_celsius_aussen", "legendFormat": "Outdoor Temp", "refId": "E"},
|
||||
{"expr": "taupi_dewpoint_celsius_innen", "legendFormat": "Indoor Dewpoint", "refId": "F"},
|
||||
{"expr": "taupi_dewpoint_celsius_aussen", "legendFormat": "Outdoor Dewpoint", "refId": "G"},
|
||||
{"expr": "taupi_humidity_percent_innen", "legendFormat": "Indoor Humidity", "refId": "H"},
|
||||
{"expr": "taupi_humidity_percent_aussen", "legendFormat": "Outdoor Humidity", "refId": "I"},
|
||||
{"expr": "taupi_critical_humidity_threshold_percent", "legendFormat": "Mold Threshold", "refId": "J"}
|
||||
{"expr": "taupi_temperature_celsius_innen", "legendFormat": "Indoor Temp", "refId": "B"},
|
||||
{"expr": "taupi_temperature_celsius_aussen", "legendFormat": "Outdoor Temp", "refId": "C"},
|
||||
{"expr": "taupi_dewpoint_celsius_innen", "legendFormat": "Indoor Dewpoint", "refId": "D"},
|
||||
{"expr": "taupi_dewpoint_celsius_aussen", "legendFormat": "Outdoor Dewpoint", "refId": "E"},
|
||||
{"expr": "taupi_humidity_percent_innen", "legendFormat": "Indoor Humidity", "refId": "F"},
|
||||
{"expr": "taupi_humidity_percent_aussen", "legendFormat": "Outdoor Humidity", "refId": "G"},
|
||||
{"expr": "taupi_target_humidity_percent", "legendFormat": "Target Setpoint", "refId": "H"},
|
||||
{"expr": "taupi_critical_humidity_threshold_percent", "legendFormat": "Mold Threshold", "refId": "I"}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"title": "📈 Deep Dive",
|
||||
"title": "💧 Psychrometric Deltas & Safety Gates",
|
||||
"type": "row",
|
||||
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 16},
|
||||
"collapsed": true,
|
||||
"panels": [
|
||||
{
|
||||
"title": "🌡️ Temperature Differential (Indoor − Outdoor)",
|
||||
"description": "Positive = cellar warmer than outside (normal). Large delta means the fan can effectively dry the cellar air by exchanging it with drier outside air.",
|
||||
"type": "timeseries",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 17},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "celsius",
|
||||
"custom": {
|
||||
"lineWidth": 2,
|
||||
"fillOpacity": 10,
|
||||
"spanNulls": true,
|
||||
"showPoints": "never",
|
||||
"gradientMode": "scheme",
|
||||
"thresholdsStyle": {"mode": "area"}
|
||||
},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{"color": "blue", "value": null},
|
||||
{"color": "green", "value": 0},
|
||||
{"color": "orange", "value": 5}
|
||||
]
|
||||
}
|
||||
}
|
||||
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 32},
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"title": "💧 Dew Point Spread (Indoor − Outdoor) — Physical Drying Potential",
|
||||
"description": "Calculates physical moisture removal capacity. When Spread > 2.0°C (Green zone), outside air is dry enough to absorb moisture from the cellar.",
|
||||
"type": "timeseries",
|
||||
"datasource": {"type": "prometheus", "uid": "${datasource}"},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 33},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "celsius",
|
||||
"custom": {
|
||||
"lineWidth": 2,
|
||||
"fillOpacity": 15,
|
||||
"spanNulls": true,
|
||||
"showPoints": "never",
|
||||
"gradientMode": "scheme",
|
||||
"thresholdsStyle": {"mode": "area"}
|
||||
},
|
||||
"options": {
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["min", "max", "mean", "lastNotNull"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{"color": "#F44336", "value": null},
|
||||
{"color": "#FFC107", "value": 0},
|
||||
{"color": "#4CAF50", "value": 2.0}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["min", "max", "mean", "lastNotNull"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_dewpoint_spread_celsius", "legendFormat": "Dewpoint Spread (In − Out)", "refId": "A"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"title": "🌡️ Temperature Delta (Indoor − Outdoor) & Heating Prevention",
|
||||
"description": "Positive = cellar warmer than outside. Summer heating rule: Fan blocked if Outdoor Temp > 20°C (or > Indoor Temp).",
|
||||
"type": "timeseries",
|
||||
"datasource": {"type": "prometheus", "uid": "${datasource}"},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 33},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "celsius",
|
||||
"custom": {
|
||||
"lineWidth": 2,
|
||||
"fillOpacity": 10,
|
||||
"spanNulls": true,
|
||||
"showPoints": "never",
|
||||
"gradientMode": "scheme",
|
||||
"thresholdsStyle": {"mode": "area"}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_temperature_celsius_innen - taupi_temperature_celsius_aussen", "legendFormat": "Temp Delta (In − Out)", "refId": "A"}
|
||||
]
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{"color": "#2196F3", "value": null},
|
||||
{"color": "#4CAF50", "value": 0},
|
||||
{"color": "#FF9800", "value": 5}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["min", "max", "mean", "lastNotNull"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_temperature_celsius_innen - taupi_temperature_celsius_aussen", "legendFormat": "Temp Delta (In − Out)", "refId": "A"}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"title": "🔋 Hardware Diagnostics & Safety Timers",
|
||||
"type": "row",
|
||||
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 41},
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"title": "🔋 BLU Sensor Batteries",
|
||||
"type": "gauge",
|
||||
"datasource": {"type": "prometheus", "uid": "${datasource}"},
|
||||
"gridPos": {"h": 7, "w": 12, "x": 0, "y": 42},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "percent",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{"color": "red", "value": null},
|
||||
{"color": "orange", "value": 20},
|
||||
{"color": "green", "value": 50}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"reduceOptions": {"values": false, "calcs": ["lastNotNull"]},
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_battery_percent_innen", "legendFormat": "Indoor Sensor Battery", "refId": "A"},
|
||||
{"expr": "taupi_battery_percent_aussen", "legendFormat": "Outdoor Sensor Battery", "refId": "B"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"title": "⏱️ Duty Cycle & Staleness Timers",
|
||||
"type": "timeseries",
|
||||
"datasource": {"type": "prometheus", "uid": "${datasource}"},
|
||||
"gridPos": {"h": 7, "w": 12, "x": 12, "y": 42},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "s",
|
||||
"custom": {
|
||||
"lineWidth": 2,
|
||||
"fillOpacity": 5,
|
||||
"spanNulls": true,
|
||||
"showPoints": "never"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "💧 Dewpoint Spread (Indoor − Outdoor)",
|
||||
"description": "When indoor dewpoint exceeds outdoor dewpoint, running the fan helps: outside air can absorb moisture. Negative = fan would make things worse.",
|
||||
"type": "timeseries",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 17},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "celsius",
|
||||
"custom": {
|
||||
"lineWidth": 2,
|
||||
"fillOpacity": 10,
|
||||
"spanNulls": true,
|
||||
"showPoints": "never",
|
||||
"gradientMode": "scheme",
|
||||
"thresholdsStyle": {"mode": "area"}
|
||||
},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{"color": "red", "value": null},
|
||||
{"color": "green", "value": 0},
|
||||
{"color": "super-light-green", "value": 3}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["min", "max", "mean", "lastNotNull"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_dewpoint_celsius_innen - taupi_dewpoint_celsius_aussen", "legendFormat": "Dewpoint Delta (In − Out)", "refId": "A"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "💧 Humidity Delta (Indoor − Outdoor)",
|
||||
"description": "How much more humid the cellar is compared to outside. High positive values indicate the fan can effectively remove moisture.",
|
||||
"type": "timeseries",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 25},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "percent",
|
||||
"custom": {
|
||||
"lineWidth": 2,
|
||||
"fillOpacity": 10,
|
||||
"spanNulls": true,
|
||||
"showPoints": "never",
|
||||
"gradientMode": "scheme",
|
||||
"thresholdsStyle": {"mode": "area"}
|
||||
},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{"color": "blue", "value": null},
|
||||
{"color": "green", "value": 0},
|
||||
{"color": "orange", "value": 15}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["min", "max", "mean", "lastNotNull"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_humidity_percent_innen - taupi_humidity_percent_aussen", "legendFormat": "Humidity Delta (In − Out)", "refId": "A"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "📉 Indoor Humidity vs Fan Runtime",
|
||||
"description": "Overlay of indoor humidity (left axis) and fan state (right axis). Shows the direct effect: when the orange fan-on band appears, does the teal humidity line drop?",
|
||||
"type": "timeseries",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 25},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"lineWidth": 2,
|
||||
"spanNulls": true,
|
||||
"showPoints": "never"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Indoor Humidity"},
|
||||
"properties": [
|
||||
{"id": "custom.axisPlacement", "value": "left"},
|
||||
{"id": "unit", "value": "percent"},
|
||||
{"id": "color", "value": {"fixedColor": "#00BFA5", "mode": "fixed"}},
|
||||
{"id": "custom.fillOpacity", "value": 10}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Fan Active"},
|
||||
"properties": [
|
||||
{"id": "custom.axisPlacement", "value": "right"},
|
||||
{"id": "custom.drawStyle", "value": "line"},
|
||||
{"id": "custom.lineInterpolation", "value": "stepAfter"},
|
||||
{"id": "custom.fillOpacity", "value": 30},
|
||||
{"id": "color", "value": {"fixedColor": "orange", "mode": "fixed"}},
|
||||
{"id": "min", "value": 0},
|
||||
{"id": "max", "value": 1},
|
||||
{"id": "decimals", "value": 0},
|
||||
{"id": "mappings", "value": [{"type": "value", "options": {"0": {"text": "OFF"}, "1": {"text": "ON"}}}]}
|
||||
]
|
||||
}
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Fan Running Time"},
|
||||
"properties": [
|
||||
{"id": "color", "value": {"fixedColor": "#FF9800", "mode": "fixed"}},
|
||||
{"id": "custom.lineWidth", "value": 2}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["min", "max", "mean", "lastNotNull"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_humidity_percent_innen", "legendFormat": "Indoor Humidity", "refId": "A"},
|
||||
{"expr": "taupi_relay_status", "legendFormat": "Fan Active", "refId": "B"}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"title": "🔋 BLE Sensors Diagnostic",
|
||||
"type": "row",
|
||||
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 17},
|
||||
"collapsed": true,
|
||||
"panels": [
|
||||
{
|
||||
"title": "Sensor Batteries",
|
||||
"type": "gauge",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 6, "w": 12, "x": 0, "y": 18},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "percent",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{"color": "red", "value": null},
|
||||
{"color": "orange", "value": 20},
|
||||
{"color": "green", "value": 50}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"reduceOptions": {"values": false, "calcs": ["lastNotNull"]},
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_battery_percent_innen", "legendFormat": "Indoor Sensor Battery", "refId": "A"},
|
||||
{"expr": "taupi_battery_percent_aussen", "legendFormat": "Outdoor Sensor Battery", "refId": "B"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Sensor Connection Age (Staleness)",
|
||||
"type": "timeseries",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 6, "w": 12, "x": 12, "y": 18},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "s",
|
||||
"custom": {
|
||||
"lineWidth": 2,
|
||||
"fillOpacity": 5,
|
||||
"spanNulls": true,
|
||||
"showPoints": "never"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_lost_connection_seconds_innen", "legendFormat": "Indoor Connection Stale Time", "refId": "A"},
|
||||
{"expr": "taupi_lost_connection_seconds_aussen", "legendFormat": "Outdoor Connection Stale Time", "refId": "B"}
|
||||
]
|
||||
}
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Cooldown Pause Remaining"},
|
||||
"properties": [
|
||||
{"id": "color", "value": {"fixedColor": "#AB47BC", "mode": "fixed"}},
|
||||
{"id": "custom.lineWidth", "value": 2}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull", "max"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_fan_running_seconds", "legendFormat": "Fan Running Time", "refId": "A"},
|
||||
{"expr": "taupi_fan_pause_remaining_seconds", "legendFormat": "Cooldown Pause Remaining", "refId": "B"},
|
||||
{"expr": "taupi_lost_connection_seconds_innen", "legendFormat": "Indoor Signal Age", "refId": "C"},
|
||||
{"expr": "taupi_lost_connection_seconds_aussen", "legendFormat": "Outdoor Signal Age", "refId": "D"}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,18 +1,80 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nextcloud-phpmyadmin
|
||||
namespace: nextcloud
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nextcloud-phpmyadmin
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nextcloud-phpmyadmin
|
||||
spec:
|
||||
containers:
|
||||
- name: phpmyadmin
|
||||
image: phpmyadmin:5.2.1
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
env:
|
||||
- name: PMA_HOST
|
||||
value: "nextcloud-mariadb"
|
||||
- name: PMA_PORT
|
||||
value: "3306"
|
||||
- name: PMA_USER
|
||||
value: "root"
|
||||
- name: PMA_PASSWORD
|
||||
value: "aeriukuow8shaiHoh4aimie5u"
|
||||
- name: PMA_ARBITRARY
|
||||
value: "0"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
memory: 256Mi
|
||||
---
|
||||
db:
|
||||
host: nextcloud-mariadb
|
||||
ingress:
|
||||
enabled: true
|
||||
hostname: "nextcloud.phpmyadmin.haumdaucher.de"
|
||||
tls: "true"
|
||||
ingressClassName: "traefik"
|
||||
# hosts:
|
||||
# - path: "/"
|
||||
# tls: true
|
||||
# name: "nextcloud.phpmyadmin.haumdaucher.de"
|
||||
# tlsSecret: "nextcloud-phpmyadmin-haumdaucher-de"
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nextcloud-phpmyadmin
|
||||
namespace: nextcloud
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
name: http
|
||||
selector:
|
||||
app: nextcloud-phpmyadmin
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: nextcloud-phpmyadmin
|
||||
namespace: nextcloud
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||
kubernetes.io/ingress.class: traefik
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
kubernetes.io/ingress.class: "traefik"
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
traefik.ingress.kubernetes.io/router.middlewares: auth-authentik-forwardauth@kubernetescrd
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
- hosts:
|
||||
- nextcloud.phpmyadmin.haumdaucher.de
|
||||
secretName: nextcloud-phpmyadmin-haumdaucher-de
|
||||
rules:
|
||||
- host: nextcloud.phpmyadmin.haumdaucher.de
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: nextcloud-phpmyadmin
|
||||
port:
|
||||
number: 80
|
||||
Binary file not shown.
Binary file not shown.
@@ -26,7 +26,7 @@ ingress:
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
traefik.ingress.kubernetes.io/router.middlewares: zigbee-zigbee2mqtt-auth@kubernetescrd
|
||||
traefik.ingress.kubernetes.io/router.middlewares: auth-authentik-forwardauth@kubernetescrd
|
||||
hosts:
|
||||
- host: zigbee2mqtt.moritzgraf.de
|
||||
paths:
|
||||
@@ -74,7 +74,7 @@ zigbee2mqtt:
|
||||
log_directories_to_keep: 100
|
||||
adapter_concurrent: 8
|
||||
adapter_delay: 100
|
||||
transmit_power: 20
|
||||
transmit_power: 5
|
||||
device_options: {}
|
||||
|
||||
extraResources:
|
||||
@@ -115,10 +115,10 @@ extraResources:
|
||||
summary: "Zigbee2MQTT Bridge Connection Offline"
|
||||
description: "The Zigbee2MQTT bridge connection is reported as offline. The bridge has lost communication with the MQTT broker or the Zigbee coordinator."
|
||||
- alert: ShellyWS90MetricsStale
|
||||
expr: absent(homeassistant_sensor_temperature_celsius{entity="sensor.shelly_ws90_temperatur"}) or (time() - max(homeassistant_last_updated_time_seconds{entity=~"sensor.shelly_ws90_.*"}) > 3600)
|
||||
expr: absent(homeassistant_sensor_temperature_celsius{entity="sensor.shelly_ws90_temperature"}) or (time() - max(homeassistant_last_updated_time_seconds{entity=~"sensor.shelly_ws90_.*"}) > 3600)
|
||||
for: 1h
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Shelly WS90 Weather Station metrics are stale"
|
||||
description: "No new metrics have been received from the Shelly WS90 weather station (sensor.shelly_ws90_temperatur) for over 1 hour. The device may be offline, out of range, or batteries might be depleted."
|
||||
description: "No new metrics have been received from the Shelly WS90 weather station (sensor.shelly_ws90_temperature) for over 1 hour. The device may be offline, out of range, or batteries might be depleted."
|
||||
|
||||
Reference in New Issue
Block a user