feat(openclaw): add shopping, supermarket deals, geizhals, and ebay MCP tools

This commit is contained in:
Moritz Graf
2026-09-10 21:32:22 +02:00
parent 2f2f597a67
commit 1e81cdbead
4754 changed files with 1298747 additions and 896 deletions
+4 -1
View File
@@ -1,5 +1,8 @@
*.secret filter=git-crypt diff=git-crypt *.secret filter=git-crypt diff=git-crypt
*.secret.yaml filter=git-crypt diff=git-crypt *.secret.yaml filter=git-crypt diff=git-crypt
*.secret.yml filter=git-crypt diff=git-crypt
*.secret.values filter=git-crypt diff=git-crypt *.secret.values filter=git-crypt diff=git-crypt
*.secret.sh filter=git-crypt diff=git-crypt *.secret.sh filter=git-crypt diff=git-crypt
*.secret.conf filter=git-crypt diff=git-crypt *.secret.conf filter=git-crypt diff=git-crypt
*.secret.auto.tfvars filter=git-crypt diff=git-crypt
*.secret.tfvars filter=git-crypt diff=git-crypt
+65
View File
@@ -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
Coder is a code server see [https://coder.com/docs/v2/latest/install/kubernetes](https://coder.com/docs/v2/latest/install/kubernetes). 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. 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
```
+65
View File
@@ -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.
+188
View File
@@ -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
```
+629
View File
@@ -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. &bull; <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**.
+24
View File
@@ -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
+62
View File
@@ -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
-25
View File
@@ -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.
+71 -10
View File
@@ -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: apiVersion: v1
host: gitea-mariadb kind: Service
ingress: metadata:
enabled: true name: gitea-db-adminer
hosts: namespace: development
- path: "/" spec:
tls: true type: ClusterIP
name: "gitea.phpmyadmin.haumdaucher.de" ports:
tlsSecret: "gitea-phpmyadmin-haumdaucher-de" - 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: annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod" 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
+65
View File
@@ -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
+62
View File
@@ -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
+35
View File
@@ -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
+6
View File
@@ -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"], "tags": ["home", "weather", "outdoor", "prometheus"],
"timezone": "browser", "timezone": "browser",
"schemaVersion": 39, "schemaVersion": 39,
"version": 3, "version": 4,
"refresh": "30s", "refresh": "30s",
"editable": false, "editable": false,
"graphTooltip": 2, "graphTooltip": 2,
@@ -29,7 +29,7 @@ data:
"datasource": { "type": "prometheus", "uid": "prometheus" }, "datasource": { "type": "prometheus", "uid": "prometheus" },
"targets": [ "targets": [
{ {
"expr": "homeassistant_sensor_unit_lqi{entity=\"sensor.shelly_ws90_linkquality\"}", "expr": "homeassistant_sensor_unit_lqi{entity=\"sensor.0xfc4d6afffe246794_linkquality\"}",
"refId": "A" "refId": "A"
} }
], ],
@@ -65,7 +65,7 @@ data:
"datasource": { "type": "prometheus", "uid": "prometheus" }, "datasource": { "type": "prometheus", "uid": "prometheus" },
"targets": [ "targets": [
{ {
"expr": "homeassistant_sensor_battery_percent{entity=\"sensor.shelly_ws90_batterie\"}", "expr": "homeassistant_sensor_battery_percent{entity=\"sensor.shelly_ws90_battery\"}",
"refId": "A" "refId": "A"
} }
], ],
@@ -100,7 +100,7 @@ data:
"datasource": { "type": "prometheus", "uid": "prometheus" }, "datasource": { "type": "prometheus", "uid": "prometheus" },
"targets": [ "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" "refId": "A"
} }
], ],
@@ -166,7 +166,7 @@ data:
"datasource": { "type": "prometheus", "uid": "prometheus" }, "datasource": { "type": "prometheus", "uid": "prometheus" },
"targets": [ "targets": [
{ {
"expr": "homeassistant_sensor_temperature_celsius{entity=\"sensor.shelly_ws90_temperatur\"}", "expr": "homeassistant_sensor_temperature_celsius{entity=\"sensor.shelly_ws90_temperature\"}",
"legendFormat": "Temperature", "legendFormat": "Temperature",
"refId": "A" "refId": "A"
}, },
@@ -191,7 +191,7 @@ data:
"refId": "E" "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", "legendFormat": "Relative Humidity",
"refId": "F" "refId": "F"
}, },
@@ -472,7 +472,7 @@ data:
"datasource": { "type": "prometheus", "uid": "prometheus" }, "datasource": { "type": "prometheus", "uid": "prometheus" },
"targets": [ "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", "legendFormat": "Pressure",
"refId": "A" "refId": "A"
}, },
@@ -12,10 +12,10 @@ data:
{ {
"uid": "taupi-fan", "uid": "taupi-fan",
"title": "💨 Cellar Fan (TAUPI)", "title": "💨 Cellar Fan (TAUPI)",
"tags": ["climate", "fan", "cellar", "prometheus"], "tags": ["climate", "fan", "cellar", "taupi", "prometheus"],
"timezone": "browser", "timezone": "browser",
"schemaVersion": 39, "schemaVersion": 41,
"version": 3, "version": 5,
"refresh": "30s", "refresh": "30s",
"editable": false, "editable": false,
"graphTooltip": 2, "graphTooltip": 2,
@@ -23,22 +23,39 @@ data:
"from": "now-24h", "from": "now-24h",
"to": "now" "to": "now"
}, },
"templating": {
"list": [
{
"name": "datasource",
"type": "datasource",
"pluginId": "prometheus",
"includeAll": false,
"label": "Prometheus",
"current": {
"selected": true,
"text": "prometheus",
"value": "prometheus"
}
}
]
},
"panels": [ "panels": [
{ {
"title": "⚡ Status & Controls", "title": "⚡ Status & Real-Time KPIs",
"type": "row", "type": "row",
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 0}, "gridPos": {"h": 1, "w": 24, "x": 0, "y": 0},
"collapsed": false "collapsed": false
}, },
{ {
"id": 1,
"title": "Fan Power Relay", "title": "Fan Power Relay",
"type": "stat", "type": "stat",
"datasource": {"type": "prometheus", "uid": "prometheus"}, "datasource": {"type": "prometheus", "uid": "${datasource}"},
"gridPos": {"h": 4, "w": 6, "x": 0, "y": 1}, "gridPos": {"h": 4, "w": 4, "x": 0, "y": 1},
"fieldConfig": { "fieldConfig": {
"defaults": { "defaults": {
"mappings": [ "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"} "color": {"mode": "thresholds"}
} }
@@ -50,18 +67,86 @@ data:
"reduceOptions": {"values": false, "calcs": ["lastNotNull"]} "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}
}, },
"targets": [ "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", "type": "stat",
"datasource": {"type": "prometheus", "uid": "prometheus"}, "datasource": {"type": "prometheus", "uid": "${datasource}"},
"gridPos": {"h": 4, "w": 6, "x": 6, "y": 1}, "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": { "fieldConfig": {
"defaults": { "defaults": {
"mappings": [ "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"]} "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}
}, },
"targets": [ "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", "type": "stat",
"datasource": {"type": "prometheus", "uid": "prometheus"}, "datasource": {"type": "prometheus", "uid": "${datasource}"},
"gridPos": {"h": 4, "w": 6, "x": 12, "y": 1}, "gridPos": {"h": 4, "w": 4, "x": 16, "y": 1},
"fieldConfig": { "fieldConfig": {
"defaults": { "defaults": {
"mappings": [ "unit": "m",
{"type": "value", "options": {"0": {"text": "NO", "color": "gray"}, "1": {"text": "YES", "color": "green"}}} "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": { "options": {
@@ -94,14 +190,15 @@ data:
"reduceOptions": {"values": false, "calcs": ["lastNotNull"]} "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}
}, },
"targets": [ "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", "type": "stat",
"datasource": {"type": "prometheus", "uid": "prometheus"}, "datasource": {"type": "prometheus", "uid": "${datasource}"},
"gridPos": {"h": 4, "w": 6, "x": 18, "y": 1}, "gridPos": {"h": 4, "w": 4, "x": 20, "y": 1},
"fieldConfig": { "fieldConfig": {
"defaults": { "defaults": {
"mappings": [ "mappings": [
@@ -116,23 +213,113 @@ data:
"reduceOptions": {"values": false, "calcs": ["lastNotNull"]} "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}
}, },
"targets": [ "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", "type": "row",
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 5}, "gridPos": {"h": 1, "w": 24, "x": 0, "y": 5},
"collapsed": false "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", "title": "📊 Complete Climate & Psychrometric 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.", "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", "type": "timeseries",
"datasource": {"type": "prometheus", "uid": "prometheus"}, "datasource": {"type": "prometheus", "uid": "${datasource}"},
"gridPos": {"h": 14, "w": 24, "x": 0, "y": 6}, "gridPos": {"h": 13, "w": 24, "x": 0, "y": 19},
"fieldConfig": { "fieldConfig": {
"defaults": { "defaults": {
"custom": { "custom": {
@@ -151,43 +338,13 @@ data:
{"id": "custom.drawStyle", "value": "line"}, {"id": "custom.drawStyle", "value": "line"},
{"id": "custom.lineInterpolation", "value": "stepAfter"}, {"id": "custom.lineInterpolation", "value": "stepAfter"},
{"id": "custom.lineWidth", "value": 0}, {"id": "custom.lineWidth", "value": 0},
{"id": "custom.fillOpacity", "value": 15}, {"id": "custom.fillOpacity", "value": 20},
{"id": "min", "value": 0}, {"id": "min", "value": 0},
{"id": "max", "value": 1}, {"id": "max", "value": 1},
{"id": "decimals", "value": 0}, {"id": "decimals", "value": 0},
{"id": "mappings", "value": [{"type": "value", "options": {"0": {"text": "OFF"}, "1": {"text": "ON"}}}]} {"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"}, "matcher": {"id": "byName", "options": "Indoor Temp"},
"properties": [ "properties": [
@@ -211,9 +368,9 @@ data:
"properties": [ "properties": [
{"id": "custom.axisPlacement", "value": "left"}, {"id": "custom.axisPlacement", "value": "left"},
{"id": "unit", "value": "celsius"}, {"id": "unit", "value": "celsius"},
{"id": "color", "value": {"fixedColor": "#FF6D00", "mode": "fixed"}}, {"id": "color", "value": {"fixedColor": "#FFB74D", "mode": "fixed"}},
{"id": "custom.lineStyle", "value": {"fill": "dash", "dash": [10, 5]}}, {"id": "custom.lineStyle", "value": {"fill": "dash", "dash": [8, 4]}},
{"id": "custom.lineWidth", "value": 1} {"id": "custom.lineWidth", "value": 1.5}
] ]
}, },
{ {
@@ -221,9 +378,9 @@ data:
"properties": [ "properties": [
{"id": "custom.axisPlacement", "value": "left"}, {"id": "custom.axisPlacement", "value": "left"},
{"id": "unit", "value": "celsius"}, {"id": "unit", "value": "celsius"},
{"id": "color", "value": {"fixedColor": "#2979FF", "mode": "fixed"}}, {"id": "color", "value": {"fixedColor": "#90CAF9", "mode": "fixed"}},
{"id": "custom.lineStyle", "value": {"fill": "dash", "dash": [10, 5]}}, {"id": "custom.lineStyle", "value": {"fill": "dash", "dash": [8, 4]}},
{"id": "custom.lineWidth", "value": 1} {"id": "custom.lineWidth", "value": 1.5}
] ]
}, },
{ {
@@ -231,9 +388,8 @@ data:
"properties": [ "properties": [
{"id": "custom.axisPlacement", "value": "right"}, {"id": "custom.axisPlacement", "value": "right"},
{"id": "unit", "value": "percent"}, {"id": "unit", "value": "percent"},
{"id": "color", "value": {"fixedColor": "#00BFA5", "mode": "fixed"}}, {"id": "color", "value": {"fixedColor": "#00E5FF", "mode": "fixed"}},
{"id": "custom.lineWidth", "value": 2}, {"id": "custom.lineWidth", "value": 2.5}
{"id": "custom.fillOpacity", "value": 5}
] ]
}, },
{ {
@@ -245,260 +401,203 @@ data:
{"id": "custom.lineWidth", "value": 1} {"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"}, "matcher": {"id": "byName", "options": "Mold Threshold"},
"properties": [ "properties": [
{"id": "custom.axisPlacement", "value": "right"}, {"id": "custom.axisPlacement", "value": "right"},
{"id": "unit", "value": "percent"}, {"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.lineStyle", "value": {"fill": "dash", "dash": [10, 5]}},
{"id": "custom.lineWidth", "value": 1} {"id": "custom.lineWidth", "value": 1.5}
] ]
} }
] ]
}, },
"options": { "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"} "tooltip": {"mode": "multi", "sort": "desc"}
}, },
"targets": [ "targets": [
{"expr": "taupi_relay_status", "legendFormat": "Fan Active", "refId": "A"}, {"expr": "taupi_relay_status", "legendFormat": "Fan Active", "refId": "A"},
{"expr": "taupi_is_critical", "legendFormat": "Mold Critical", "refId": "B"}, {"expr": "taupi_temperature_celsius_innen", "legendFormat": "Indoor Temp", "refId": "B"},
{"expr": "taupi_would_fan_activate", "legendFormat": "Activation Criteria", "refId": "C"}, {"expr": "taupi_temperature_celsius_aussen", "legendFormat": "Outdoor Temp", "refId": "C"},
{"expr": "taupi_temperature_celsius_innen", "legendFormat": "Indoor Temp", "refId": "D"}, {"expr": "taupi_dewpoint_celsius_innen", "legendFormat": "Indoor Dewpoint", "refId": "D"},
{"expr": "taupi_temperature_celsius_aussen", "legendFormat": "Outdoor Temp", "refId": "E"}, {"expr": "taupi_dewpoint_celsius_aussen", "legendFormat": "Outdoor Dewpoint", "refId": "E"},
{"expr": "taupi_dewpoint_celsius_innen", "legendFormat": "Indoor Dewpoint", "refId": "F"}, {"expr": "taupi_humidity_percent_innen", "legendFormat": "Indoor Humidity", "refId": "F"},
{"expr": "taupi_dewpoint_celsius_aussen", "legendFormat": "Outdoor Dewpoint", "refId": "G"}, {"expr": "taupi_humidity_percent_aussen", "legendFormat": "Outdoor Humidity", "refId": "G"},
{"expr": "taupi_humidity_percent_innen", "legendFormat": "Indoor Humidity", "refId": "H"}, {"expr": "taupi_target_humidity_percent", "legendFormat": "Target Setpoint", "refId": "H"},
{"expr": "taupi_humidity_percent_aussen", "legendFormat": "Outdoor Humidity", "refId": "I"}, {"expr": "taupi_critical_humidity_threshold_percent", "legendFormat": "Mold Threshold", "refId": "I"}
{"expr": "taupi_critical_humidity_threshold_percent", "legendFormat": "Mold Threshold", "refId": "J"}
] ]
}, },
{ {
"title": "📈 Deep Dive", "title": "💧 Psychrometric Deltas & Safety Gates",
"type": "row", "type": "row",
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 16}, "gridPos": {"h": 1, "w": 24, "x": 0, "y": 32},
"collapsed": true, "collapsed": false
"panels": [ },
{ {
"title": "🌡️ Temperature Differential (Indoor − Outdoor)", "id": 9,
"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.", "title": "💧 Dew Point Spread (Indoor − Outdoor) — Physical Drying Potential",
"type": "timeseries", "description": "Calculates physical moisture removal capacity. When Spread > 2.0°C (Green zone), outside air is dry enough to absorb moisture from the cellar.",
"datasource": {"type": "prometheus", "uid": "prometheus"}, "type": "timeseries",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 17}, "datasource": {"type": "prometheus", "uid": "${datasource}"},
"fieldConfig": { "gridPos": {"h": 8, "w": 12, "x": 0, "y": 33},
"defaults": { "fieldConfig": {
"unit": "celsius", "defaults": {
"custom": { "unit": "celsius",
"lineWidth": 2, "custom": {
"fillOpacity": 10, "lineWidth": 2,
"spanNulls": true, "fillOpacity": 15,
"showPoints": "never", "spanNulls": true,
"gradientMode": "scheme", "showPoints": "never",
"thresholdsStyle": {"mode": "area"} "gradientMode": "scheme",
}, "thresholdsStyle": {"mode": "area"}
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "blue", "value": null},
{"color": "green", "value": 0},
{"color": "orange", "value": 5}
]
}
}
}, },
"options": { "thresholds": {
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["min", "max", "mean", "lastNotNull"]}, "mode": "absolute",
"tooltip": {"mode": "multi", "sort": "desc"} "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": [ "thresholds": {
{"expr": "taupi_temperature_celsius_innen - taupi_temperature_celsius_aussen", "legendFormat": "Temp Delta (In − Out)", "refId": "A"} "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"
}
}, },
{ "overrides": [
"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.", "matcher": {"id": "byName", "options": "Fan Running Time"},
"type": "timeseries", "properties": [
"datasource": {"type": "prometheus", "uid": "prometheus"}, {"id": "color", "value": {"fixedColor": "#FF9800", "mode": "fixed"}},
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 17}, {"id": "custom.lineWidth", "value": 2}
"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"}}}]}
]
}
] ]
}, },
"options": { {
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["min", "max", "mean", "lastNotNull"]}, "matcher": {"id": "byName", "options": "Cooldown Pause Remaining"},
"tooltip": {"mode": "multi", "sort": "desc"} "properties": [
}, {"id": "color", "value": {"fixedColor": "#AB47BC", "mode": "fixed"}},
"targets": [ {"id": "custom.lineWidth", "value": 2}
{"expr": "taupi_humidity_percent_innen", "legendFormat": "Indoor Humidity", "refId": "A"}, ]
{"expr": "taupi_relay_status", "legendFormat": "Fan Active", "refId": "B"} }
] ]
} },
] "options": {
}, "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull", "max"]},
"tooltip": {"mode": "multi", "sort": "desc"}
{ },
"title": "🔋 BLE Sensors Diagnostic", "targets": [
"type": "row", {"expr": "taupi_fan_running_seconds", "legendFormat": "Fan Running Time", "refId": "A"},
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 17}, {"expr": "taupi_fan_pause_remaining_seconds", "legendFormat": "Cooldown Pause Remaining", "refId": "B"},
"collapsed": true, {"expr": "taupi_lost_connection_seconds_innen", "legendFormat": "Indoor Signal Age", "refId": "C"},
"panels": [ {"expr": "taupi_lost_connection_seconds_aussen", "legendFormat": "Outdoor Signal Age", "refId": "D"}
{
"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"}
]
}
] ]
} }
] ]
+77 -15
View File
@@ -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: apiVersion: v1
host: nextcloud-mariadb kind: Service
ingress: metadata:
enabled: true name: nextcloud-phpmyadmin
hostname: "nextcloud.phpmyadmin.haumdaucher.de" namespace: nextcloud
tls: "true" spec:
ingressClassName: "traefik" type: ClusterIP
# hosts: ports:
# - path: "/" - port: 80
# tls: true targetPort: 80
# name: "nextcloud.phpmyadmin.haumdaucher.de" name: http
# tlsSecret: "nextcloud-phpmyadmin-haumdaucher-de" selector:
app: nextcloud-phpmyadmin
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nextcloud-phpmyadmin
namespace: nextcloud
annotations: annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod" cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/proxy-body-size: "0" kubernetes.io/ingress.class: "traefik"
kubernetes.io/ingress.class: traefik nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
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.
+4 -4
View File
@@ -26,7 +26,7 @@ ingress:
nginx.ingress.kubernetes.io/proxy-body-size: "0" nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-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: hosts:
- host: zigbee2mqtt.moritzgraf.de - host: zigbee2mqtt.moritzgraf.de
paths: paths:
@@ -74,7 +74,7 @@ zigbee2mqtt:
log_directories_to_keep: 100 log_directories_to_keep: 100
adapter_concurrent: 8 adapter_concurrent: 8
adapter_delay: 100 adapter_delay: 100
transmit_power: 20 transmit_power: 5
device_options: {} device_options: {}
extraResources: extraResources:
@@ -115,10 +115,10 @@ extraResources:
summary: "Zigbee2MQTT Bridge Connection Offline" 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." description: "The Zigbee2MQTT bridge connection is reported as offline. The bridge has lost communication with the MQTT broker or the Zigbee coordinator."
- alert: ShellyWS90MetricsStale - 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 for: 1h
labels: labels:
severity: warning severity: warning
annotations: annotations:
summary: "Shelly WS90 Weather Station metrics are stale" 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."
+32 -32
View File
@@ -1,24 +1,24 @@
# This file is maintained automatically by "terraform init". # This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates. # Manual edits may be lost in future updates.
provider "registry.opentofu.org/ovh/ovh" { provider "registry.terraform.io/goauthentik/authentik" {
version = "2.1.0" version = "2026.5.1"
constraints = ">= 2024.12.0"
hashes = [ hashes = [
"h1:QNYWbHCXQyUAs1Ucxd4Lw7CS/FohMP/P21uyRWlYPTw=", "h1:sa790BNXc5pY8vVXyFn8Y1cHY+cRDbEiHDsr9t0NdiA=",
"zh:08d56f6fd7e0730ea379962b869a80ee47992d5f8523d259eada4479001ce646", "zh:05f252734db99792e5d3f52b582a4e0027348ac2614d8c9621bf7e623cff8036",
"zh:0f1e34db1f51e5b05e183b8905e89c521e223481507ab72421ec141718cb1465", "zh:0e1cd8041650aed9bb8d4af23301e786223ed5d1a00c2a5150b7ad874ae03917",
"zh:299b1a702fba2b42bcef9a12db0cb51d091962a6387e1da6043b4830d60b7eca", "zh:26b1daebd30650df411f903248e061cf963712a063b38e731782fb13a5755ddf",
"zh:4ee79a677eddff27ceed33c207b074ebbfc33727a9e5b863ae8d800da819f1da", "zh:362b536ff6bf8866c7c1094c3e2322c22b8c36d8d4caa4db0397cebbcad08561",
"zh:50b7ae60d4d539b79e8db097db954a03e582d29c760cff0854a9b882148db848", "zh:3d94367b853960f5e88efe3ae69d995d02a11dc8d60ec4d7413a37aabf78b379",
"zh:6121ce09a73ed20025e933a8e237cbc62ccaee92fed5ebcbb6984b3c2bc6e4fc", "zh:3e840e204677eabe562d17c2ad12afdfa250098725bebac4e91fb8934e233c22",
"zh:75cccdfefdfd9054b388b6c8ddce3ffa76adbdc9eb149f6e34998a87e5bb0d3d", "zh:4db66139d7a9ec6f7852538573261010d48f0a939e2fe8eeb5c55b01149e3629",
"zh:8859bdfb25ae26e4e924e9909f476eafea91a9c2c9f7f65fbc85e68f3c2c4e60", "zh:7b88a830fb26c697f9d240f498f604485454b12e56b7604eda4ca35d10660ecd",
"zh:a435fe28514806bc7b3a3c5a9b878401790402bd75d34f1a9fe1b7b7db9b66b7", "zh:851bc8bd2d4a16dc71f30da17b6285aa7d85f4bc4175fde7b8319a6a8987ec65",
"zh:b3ac7469d1864cf96cad90fd8b0c4b99da2e5695b6b2826540320302c241f803", "zh:8bdfc2c70271d48126eef3b1a30d0d132e868d2118ae7277323ed4c905636e4a",
"zh:b531d3ea1b12a17a8999a093a819f0e90c18b0d5db371e2c7e15add337f421ff", "zh:e85c03eac43f23f25ad5443a2984cc67977c3abdcb4c4c426d37342c2f25c37e",
"zh:bebc6b1aa48fc5c42fd68d9ff2f8866d794bfff568486d1943827202212aed44", "zh:efc296826a700a4e90ee06f8bd3b137617486ce774772aabed3053d4ef8dd906",
"zh:d4fb1c9e76eb923b35bea62ba4921cba63da83abe4676150d7a4bb9b687228eb", "zh:f249033dc242c51600cd3968535efa0780352d5ae7435e8ab1482a7ccc9c2f5e",
"zh:f953ef37e4a1b3cac22f851c3269e7376704018ffa642b4768869cc227ffccc9",
] ]
} }
@@ -84,21 +84,21 @@ provider "registry.terraform.io/hashicorp/random" {
} }
provider "registry.terraform.io/ovh/ovh" { provider "registry.terraform.io/ovh/ovh" {
version = "2.15.0" version = "2.19.0"
hashes = [ hashes = [
"h1:Y6pQdKjn1OR4wNlxRpy+nI7Dvap/bKzFb68fynMxTmI=", "h1:SUzZY0kbQQ1Z+T958n8//7u+UlItQgll3vTfxuUbdOM=",
"zh:22d361f99caa70b07532fce13208fce8b84072cf83eaf6ddfcd9096df88b57ea", "zh:0ba0eec9b66d3cf3ffbb8cc753d87aebc7ddb73b975c5b2eb372ec1fcf8712fe",
"zh:497f88936649d3c352fdb639ba5ebdabfec8ec7a59d67bf619013e3afbbaefb4", "zh:24773d4959b2fa2674760eebdda421e93a7b0e4cadf1ae135ed66d573e86b040",
"zh:62e49208e2950daf366b396694dc7de5bba31a8b7a10aac7c021f2c78fd64f70", "zh:2f24103c6b132cb21f1834ee2b00ebce863118d53dc2e55b85cd5b1d154bdc46",
"zh:97e06f7494706df30c136f4809314d92a25129738cf080e22c5c9e47ec70c4cf", "zh:3a7492ea842566601323b6ff4ee45a6ced7eb7c08ed888608acc3bc8e490c22a",
"zh:be23cf1ecb2eb76c60dad72eb4f9d005c6184cae6e2cf9f13b0462030133644c", "zh:5018841095e9677fc8e5abcdd8336b5ad20086426cc22be35fb657b3cd8c7b9d",
"zh:bf60c1af6db5ca51ff4372fee8e64a21462134d281ae9cb3968ca5b209338c4b", "zh:62ea15a24fb26a79586c14eed2d1b8dfbdb5195288620dcd36d9c71a55b5860c",
"zh:c9e5ad5c063249786595c5b6c3f59791bd21f5157ff0572a4ed2d52ca8141c50", "zh:6847b431185ddf4e7349d2397ac9cb1659ad129b01ec1661df2cac60b031e6c4",
"zh:d2bf2603741df6fc29a88384f7026fb8b0e982b8a270796e672037d287b41e83", "zh:7cdc7f403f10b405ef5e9a10094ce11685d2a739058e5c21c7db268dcf3c8fd8",
"zh:d944de8032fc01d8488087755bf7fb813e7b8c259e23a7b68ed654905ad19b56", "zh:9d9ae5b6afe707487230c46930a805b0bcfc4c95cb779c4dd956695dd67a7a84",
"zh:dcf2a7620abac38e9d8f0a88f2a79de0fddd917ba0facba1108f6581eb1b7f6f", "zh:bcc1bfd1df2546aff09726885703713dc9579f73240395cb9890140165f45d1c",
"zh:e7c7905954f69f0d5b6cf9bd87572101924e128aa33311856dbbfedd711361d2", "zh:c02e93bdd854b5814089d88c2ecf76fc22b83dda1c4785aea6765c1b72ac9897",
"zh:f6a9977d744ed879a0b3c28a5a800b6a1f49aabe8d71e87ca1d02fe6a77a31ba", "zh:e26191ff7f5ae7750d129136c0e6f13a54167fe332dc6c5f3f3ceab17c27c4df",
"zh:fd0e48de826df04e18489d33b0ff35bd776cb6e7704188f67dad66cbb041e5af", "zh:f0de0de40649efc805e63fbe405055ed4c0f8149b0d530bb63d079b62f692de1",
] ]
} }
Binary file not shown.
+725
View File
@@ -0,0 +1,725 @@
# --- Authentik Terraform Configuration ---
variable "authentik_url" {
description = "Base URL of the Authentik instance"
type = string
default = "https://auth.haumdaucher.de"
}
variable "authentik_token" {
description = "Authentik API Token (create via Directory > Tokens & App passwords)"
type = string
sensitive = true
default = null
}
provider "authentik" {
url = var.authentik_url
token = var.authentik_token
}
# --- Branding: Haumdaucher Custom Theme & Assets ---
resource "authentik_brand" "haumdaucher" {
domain = "auth.haumdaucher.de"
default = true
branding_title = "HAUMDAUCHER"
branding_logo = "https://haumdaucher.de/images/logo_classic.png"
branding_favicon = "https://haumdaucher.de/icon-192.png"
branding_custom_css = file("${path.module}/haumdaucher-theme.css")
}
# --- Directory: User Groups ---
resource "authentik_group" "infra_admins" {
name = "infra-admins"
is_superuser = false
}
resource "authentik_group" "users" {
name = "users"
is_superuser = false
}
resource "authentik_group" "verein_mitglieder" {
name = "verein-mitglieder"
is_superuser = false
}
# --- Flows: Authorization, Invalidation & Custom Invitation Flow ---
data "authentik_flow" "default_authorization_flow" {
slug = "default-provider-authorization-implicit-consent"
}
data "authentik_flow" "default_invalidation_flow" {
slug = "default-invalidation-flow"
}
# 1. Prompt Fields for Member Onboarding
resource "authentik_stage_prompt_field" "haumdaucher_enrollment_username" {
name = "haumdaucher-enrollment-username"
field_key = "username"
label = "Benutzername"
type = "username"
required = true
order = 10
}
resource "authentik_stage_prompt_field" "haumdaucher_enrollment_email" {
name = "haumdaucher-enrollment-email"
field_key = "email"
label = "E-Mail-Adresse"
type = "email"
required = true
order = 15
}
resource "authentik_stage_prompt_field" "haumdaucher_enrollment_name" {
name = "haumdaucher-enrollment-name"
field_key = "name"
label = "Vollständiger Name"
type = "text"
required = true
order = 20
}
resource "authentik_stage_prompt_field" "haumdaucher_enrollment_password" {
name = "haumdaucher-enrollment-password"
field_key = "password"
label = "Passwort"
type = "password"
required = true
order = 30
}
resource "authentik_stage_prompt_field" "haumdaucher_enrollment_password_repeat" {
name = "haumdaucher-enrollment-password-repeat"
field_key = "password_repeat"
label = "Passwort wiederholen"
type = "password"
required = true
order = 40
}
# 2. Prompt Stage
resource "authentik_stage_prompt" "haumdaucher_enrollment_prompt" {
name = "haumdaucher-enrollment-prompt"
fields = [
authentik_stage_prompt_field.haumdaucher_enrollment_username.id,
authentik_stage_prompt_field.haumdaucher_enrollment_email.id,
authentik_stage_prompt_field.haumdaucher_enrollment_name.id,
authentik_stage_prompt_field.haumdaucher_enrollment_password.id,
authentik_stage_prompt_field.haumdaucher_enrollment_password_repeat.id,
]
}
# 3. Invitation Stage (gates the flow so only valid ?itoken=... can register)
resource "authentik_stage_invitation" "haumdaucher_enrollment_invitation" {
name = "haumdaucher-enrollment-invitation"
continue_flow_without_invitation = false
}
# 4. User Write Stage (creates user in database and assigns default 'users' group)
resource "authentik_stage_user_write" "haumdaucher_enrollment_write" {
name = "haumdaucher-enrollment-write"
create_users_as_inactive = false
create_users_group = authentik_group.users.id
}
# 5. User Login Stage (starts the user session)
resource "authentik_stage_user_login" "haumdaucher_enrollment_login" {
name = "haumdaucher-enrollment-login"
session_duration = "seconds=0"
}
# 6. Redirect Stage (redirects user to haumdaucher.de after successful registration)
resource "authentik_stage_redirect" "haumdaucher_enrollment_redirect" {
name = "haumdaucher-enrollment-redirect"
mode = "static"
target_static = "https://haumdaucher.de"
}
# 7. Flow Definition
resource "authentik_flow" "haumdaucher_invitation_flow" {
name = "Haumdaucher Invitation Flow"
slug = "haumdaucher-invitation-flow"
title = "Haumdaucher Registrierung"
designation = "enrollment"
}
# 8. Flow Stage Bindings (Execution Order)
resource "authentik_flow_stage_binding" "bind_haumdaucher_invitation" {
target = authentik_flow.haumdaucher_invitation_flow.uuid
stage = authentik_stage_invitation.haumdaucher_enrollment_invitation.id
order = 10
}
resource "authentik_flow_stage_binding" "bind_haumdaucher_prompt" {
target = authentik_flow.haumdaucher_invitation_flow.uuid
stage = authentik_stage_prompt.haumdaucher_enrollment_prompt.id
order = 20
}
resource "authentik_flow_stage_binding" "bind_haumdaucher_write" {
target = authentik_flow.haumdaucher_invitation_flow.uuid
stage = authentik_stage_user_write.haumdaucher_enrollment_write.id
order = 30
}
resource "authentik_flow_stage_binding" "bind_haumdaucher_login" {
target = authentik_flow.haumdaucher_invitation_flow.uuid
stage = authentik_stage_user_login.haumdaucher_enrollment_login.id
order = 40
}
resource "authentik_flow_stage_binding" "bind_haumdaucher_redirect" {
target = authentik_flow.haumdaucher_invitation_flow.uuid
stage = authentik_stage_redirect.haumdaucher_enrollment_redirect.id
order = 50
}
# --- ForwardAuth Proxy Providers & Applications ---
# 1. Zigbee2MQTT
resource "authentik_provider_proxy" "zigbee2mqtt" {
name = "Zigbee2MQTT"
external_host = "https://zigbee2mqtt.moritzgraf.de"
mode = "forward_single"
authorization_flow = data.authentik_flow.default_authorization_flow.id
invalidation_flow = data.authentik_flow.default_invalidation_flow.id
intercept_header_auth = true
}
resource "authentik_application" "zigbee2mqtt" {
name = "Zigbee2MQTT"
slug = "zigbee2mqtt"
protocol_provider = authentik_provider_proxy.zigbee2mqtt.id
}
# 2. Prometheus
resource "authentik_provider_proxy" "prometheus" {
name = "Prometheus"
external_host = "https://prometheus.haumdaucher.de"
mode = "forward_single"
authorization_flow = data.authentik_flow.default_authorization_flow.id
invalidation_flow = data.authentik_flow.default_invalidation_flow.id
intercept_header_auth = true
}
resource "authentik_application" "prometheus" {
name = "Prometheus"
slug = "prometheus"
protocol_provider = authentik_provider_proxy.prometheus.id
}
# 3. Alertmanager
resource "authentik_provider_proxy" "alertmanager" {
name = "Alertmanager"
external_host = "https://alertmanager.haumdaucher.de"
mode = "forward_single"
authorization_flow = data.authentik_flow.default_authorization_flow.id
invalidation_flow = data.authentik_flow.default_invalidation_flow.id
intercept_header_auth = true
}
resource "authentik_application" "alertmanager" {
name = "Alertmanager"
slug = "alertmanager"
protocol_provider = authentik_provider_proxy.alertmanager.id
}
# 4. Nextcloud phpMyAdmin
resource "authentik_provider_proxy" "nextcloud_phpmyadmin" {
name = "Nextcloud phpMyAdmin"
external_host = "https://nextcloud.phpmyadmin.haumdaucher.de"
mode = "forward_single"
authorization_flow = data.authentik_flow.default_authorization_flow.id
invalidation_flow = data.authentik_flow.default_invalidation_flow.id
intercept_header_auth = true
}
resource "authentik_application" "nextcloud_phpmyadmin" {
name = "Nextcloud phpMyAdmin"
slug = "nextcloud-phpmyadmin"
protocol_provider = authentik_provider_proxy.nextcloud_phpmyadmin.id
}
# 5. Gitea phpMyAdmin
resource "authentik_provider_proxy" "gitea_phpmyadmin" {
name = "Gitea phpMyAdmin"
external_host = "https://gitea.phpmyadmin.haumdaucher.de"
mode = "forward_single"
authorization_flow = data.authentik_flow.default_authorization_flow.id
invalidation_flow = data.authentik_flow.default_invalidation_flow.id
intercept_header_auth = true
}
resource "authentik_application" "gitea_phpmyadmin" {
name = "Gitea phpMyAdmin"
slug = "gitea-phpmyadmin"
protocol_provider = authentik_provider_proxy.gitea_phpmyadmin.id
}
# 6. Home Assistant VS Code
resource "authentik_provider_proxy" "hass_coder" {
name = "Home Assistant Code"
external_host = "https://hass-coder.moritzgraf.de"
mode = "forward_single"
authorization_flow = data.authentik_flow.default_authorization_flow.id
invalidation_flow = data.authentik_flow.default_invalidation_flow.id
intercept_header_auth = true
}
resource "authentik_application" "hass_coder" {
name = "Home Assistant Code"
slug = "hass-coder"
protocol_provider = authentik_provider_proxy.hass_coder.id
}
# 7. OpenClaw
resource "authentik_provider_proxy" "openclaw" {
name = "OpenClaw"
external_host = "https://openclaw.haumdaucher.de"
mode = "forward_single"
authorization_flow = data.authentik_flow.default_authorization_flow.id
invalidation_flow = data.authentik_flow.default_invalidation_flow.id
intercept_header_auth = true
}
resource "authentik_application" "openclaw" {
name = "OpenClaw"
slug = "openclaw"
protocol_provider = authentik_provider_proxy.openclaw.id
}
# 8. n8n
resource "authentik_provider_proxy" "n8n" {
name = "n8n"
external_host = "https://n8n.moritzgraf.de"
mode = "forward_single"
authorization_flow = data.authentik_flow.default_authorization_flow.id
invalidation_flow = data.authentik_flow.default_invalidation_flow.id
intercept_header_auth = true
}
resource "authentik_application" "n8n" {
name = "n8n"
slug = "n8n"
protocol_provider = authentik_provider_proxy.n8n.id
}
# --- Outpost: Embedded Proxy Outpost ---
resource "authentik_outpost" "embedded" {
name = "authentik Embedded Outpost"
type = "proxy"
protocol_providers = [
authentik_provider_proxy.zigbee2mqtt.id,
authentik_provider_proxy.prometheus.id,
authentik_provider_proxy.alertmanager.id,
authentik_provider_proxy.nextcloud_phpmyadmin.id,
authentik_provider_proxy.gitea_phpmyadmin.id,
authentik_provider_proxy.hass_coder.id,
authentik_provider_proxy.openclaw.id,
authentik_provider_proxy.n8n.id,
]
}
# --- Machine & AI Assistant: Service Account & API Token ---
resource "authentik_user" "ai_assistant" {
username = "ai-assistant"
name = "AI Assistant"
type = "service_account"
groups = [authentik_group.infra_admins.id]
}
resource "authentik_token" "ai_assistant_token" {
identifier = "ai-assistant-api-token"
user = authentik_user.ai_assistant.id
intent = "api"
expiring = false
retrieve_key = true
}
# --- Native OIDC Providers & Applications ---
# Scope Mappings
data "authentik_property_mapping_provider_scope" "openid" {
scope_name = "openid"
}
data "authentik_property_mapping_provider_scope" "email" {
scope_name = "email"
}
data "authentik_property_mapping_provider_scope" "profile" {
scope_name = "profile"
}
data "authentik_certificate_key_pair" "generated" {
name = "authentik Self-signed Certificate"
}
# 1. Gitea
resource "random_password" "gitea_oauth_client_secret" {
length = 40
special = false
}
resource "authentik_provider_oauth2" "gitea" {
name = "Gitea"
client_id = "gitea"
client_secret = random_password.gitea_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
grant_types = ["authorization_code", "refresh_token"]
allowed_redirect_uris = [
{
matching_mode = "strict"
url = "https://git.moritzgraf.de/user/oauth2/authentik/callback"
},
{
matching_mode = "strict"
url = "https://git.moritzgraf.de/user/oauth2/Authentik/callback"
}
]
sub_mode = "user_username"
include_claims_in_id_token = true
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,
]
}
resource "authentik_application" "gitea" {
name = "Gitea"
slug = "gitea"
protocol_provider = authentik_provider_oauth2.gitea.id
}
output "gitea_oauth_client_secret" {
description = "OAuth2 Client Secret for Gitea OIDC provider"
value = random_password.gitea_oauth_client_secret.result
sensitive = true
}
# 2. Grafana
resource "random_password" "grafana_oauth_client_secret" {
length = 40
special = false
}
resource "authentik_provider_oauth2" "grafana" {
name = "Grafana"
client_id = "grafana"
client_secret = random_password.grafana_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
grant_types = ["authorization_code", "refresh_token"]
allowed_redirect_uris = [
{
matching_mode = "strict"
url = "https://grafana.haumdaucher.de/login/generic_oauth"
}
]
sub_mode = "user_username"
include_claims_in_id_token = true
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,
]
}
resource "authentik_application" "grafana" {
name = "Grafana"
slug = "grafana"
protocol_provider = authentik_provider_oauth2.grafana.id
}
output "grafana_oauth_client_secret" {
description = "OAuth2 Client Secret for Grafana OIDC provider"
value = random_password.grafana_oauth_client_secret.result
sensitive = true
}
# 3. Coder v2
resource "random_password" "coder_oauth_client_secret" {
length = 40
special = false
}
resource "authentik_provider_oauth2" "coder" {
name = "Coder"
client_id = "coder"
client_secret = random_password.coder_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
grant_types = ["authorization_code", "refresh_token"]
allowed_redirect_uris = [
{
matching_mode = "strict"
url = "https://coder.moritzgraf.de/api/v2/users/oidc/callback"
}
]
sub_mode = "user_username"
include_claims_in_id_token = true
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,
]
}
resource "authentik_application" "coder" {
name = "Coder"
slug = "coder"
protocol_provider = authentik_provider_oauth2.coder.id
}
output "coder_oauth_client_secret" {
description = "OAuth2 Client Secret for Coder OIDC provider"
value = random_password.coder_oauth_client_secret.result
sensitive = true
}
# 4. MinIO Console
resource "authentik_property_mapping_provider_scope" "minio_policy" {
name = "minio-policy"
scope_name = "policy"
expression = <<EOF
if "infra-admins" in [group.name for group in request.user.groups.all()]:
return {"policy": "consoleAdmin"}
return {"policy": "readonly"}
EOF
}
resource "random_password" "minio_oauth_client_secret" {
length = 40
special = false
}
resource "authentik_provider_oauth2" "minio" {
name = "MinIO"
client_id = "minio"
client_secret = random_password.minio_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
grant_types = ["authorization_code", "refresh_token"]
allowed_redirect_uris = [
{
matching_mode = "strict"
url = "https://minio-console.haumdaucher.de/oauth_callback"
}
]
sub_mode = "user_username"
include_claims_in_id_token = true
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.minio_policy.id,
]
}
resource "authentik_application" "minio" {
name = "MinIO"
slug = "minio"
protocol_provider = authentik_provider_oauth2.minio.id
}
output "minio_oauth_client_secret" {
description = "OAuth2 Client Secret for MinIO OIDC provider"
value = random_password.minio_oauth_client_secret.result
sensitive = true
}
# 5. Nextcloud
resource "random_password" "nextcloud_oauth_client_secret" {
length = 40
special = false
}
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
grant_types = ["authorization_code", "refresh_token"]
allowed_redirect_uris = [
{
matching_mode = "strict"
url = "https://cloud.haumdaucher.de/apps/user_oidc/code"
}
]
sub_mode = "user_username"
include_claims_in_id_token = true
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,
]
}
resource "authentik_application" "nextcloud" {
name = "Nextcloud"
slug = "nextcloud"
protocol_provider = authentik_provider_oauth2.nextcloud.id
}
output "nextcloud_oauth_client_secret" {
description = "OAuth2 Client Secret for Nextcloud OIDC provider"
value = random_password.nextcloud_oauth_client_secret.result
sensitive = true
}
# 6. Home Assistant
resource "random_password" "homeassistant_oauth_client_secret" {
length = 40
special = false
}
resource "authentik_provider_oauth2" "homeassistant" {
name = "Home Assistant"
client_id = "homeassistant"
client_secret = random_password.homeassistant_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
grant_types = ["authorization_code", "refresh_token"]
allowed_redirect_uris = [
{
matching_mode = "strict"
url = "https://hass.moritzgraf.de/auth/oidc/callback"
},
{
matching_mode = "strict"
url = "https://hass.moritzgraf.de/auth/external/callback"
}
]
sub_mode = "user_username"
include_claims_in_id_token = true
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,
]
}
resource "authentik_application" "homeassistant" {
name = "Home Assistant"
slug = "homeassistant"
protocol_provider = authentik_provider_oauth2.homeassistant.id
}
output "homeassistant_oauth_client_secret" {
description = "OAuth2 Client Secret for Home Assistant OIDC provider"
value = random_password.homeassistant_oauth_client_secret.result
sensitive = true
}
# --- Application Policy Bindings (Access Control & RBAC) ---
# 1. Admin-Only Applications (Strictly restricted 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
}
resource "authentik_policy_binding" "prometheus_admin_only" {
target = authentik_application.prometheus.uuid
group = authentik_group.infra_admins.id
order = 0
}
resource "authentik_policy_binding" "alertmanager_admin_only" {
target = authentik_application.alertmanager.uuid
group = authentik_group.infra_admins.id
order = 0
}
resource "authentik_policy_binding" "nextcloud_phpmyadmin_admin_only" {
target = authentik_application.nextcloud_phpmyadmin.uuid
group = authentik_group.infra_admins.id
order = 0
}
resource "authentik_policy_binding" "gitea_phpmyadmin_admin_only" {
target = authentik_application.gitea_phpmyadmin.uuid
group = authentik_group.infra_admins.id
order = 0
}
resource "authentik_policy_binding" "hass_coder_admin_only" {
target = authentik_application.hass_coder.uuid
group = authentik_group.infra_admins.id
order = 0
}
resource "authentik_policy_binding" "n8n_admin_only" {
target = authentik_application.n8n.uuid
group = authentik_group.infra_admins.id
order = 0
}
resource "authentik_policy_binding" "coder_admin_only" {
target = authentik_application.coder.uuid
group = authentik_group.infra_admins.id
order = 0
}
resource "authentik_policy_binding" "minio_admin_only" {
target = authentik_application.minio.uuid
group = authentik_group.infra_admins.id
order = 0
}
# 2. Shared & Member Applications (Accessible to all authenticated users)
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
}
resource "authentik_policy_binding" "gitea_users" {
target = authentik_application.gitea.uuid
group = authentik_group.users.id
order = 0
}
resource "authentik_policy_binding" "homeassistant_users" {
target = authentik_application.homeassistant.uuid
group = authentik_group.users.id
order = 0
}
+40 -40
View File
@@ -2,10 +2,10 @@
resource "ovh_domain_zone_record" "haumdaucher_mx_record" { resource "ovh_domain_zone_record" "haumdaucher_mx_record" {
zone = "haumdaucher.de" zone = "haumdaucher.de"
fieldtype = "MX" fieldtype = "MX"
ttl = 60 ttl = 60
target = "1 SMTP.GOOGLE.COM." target = "1 SMTP.GOOGLE.COM."
} }
resource "ovh_domain_zone_record" "spf_haumdaucher_de" { resource "ovh_domain_zone_record" "spf_haumdaucher_de" {
@@ -36,18 +36,18 @@ resource "ovh_domain_zone_record" "dkim_google_haumdaucher_de" {
resource "ovh_domain_zone_record" "mx_record_moritzgraf_de" { resource "ovh_domain_zone_record" "mx_record_moritzgraf_de" {
zone = "moritzgraf.de" zone = "moritzgraf.de"
fieldtype = "MX" fieldtype = "MX"
ttl = 60 ttl = 60
target = "1 SMTP.GOOGLE.COM." target = "1 SMTP.GOOGLE.COM."
} }
resource "ovh_domain_zone_record" "google_domain_verification_moritzgraf_de" { resource "ovh_domain_zone_record" "google_domain_verification_moritzgraf_de" {
zone = "moritzgraf.de" zone = "moritzgraf.de"
fieldtype = "TXT" fieldtype = "TXT"
ttl = 60 ttl = 60
target = "google-site-verification=YwUd-Bmbq1-lA7uYGcowsLEMczqx1odndFZq_Nk3nvM" target = "google-site-verification=YwUd-Bmbq1-lA7uYGcowsLEMczqx1odndFZq_Nk3nvM"
} }
resource "ovh_domain_zone_record" "spf_moritzgraf_de" { resource "ovh_domain_zone_record" "spf_moritzgraf_de" {
@@ -79,17 +79,17 @@ resource "ovh_domain_zone_record" "dkim_google_moritzgraf_de" {
resource "ovh_domain_zone_record" "mx_record_ameliegraf_de" { resource "ovh_domain_zone_record" "mx_record_ameliegraf_de" {
zone = "ameliegraf.de" zone = "ameliegraf.de"
fieldtype = "MX" fieldtype = "MX"
ttl = 60 ttl = 60
target = "1 smtp.google.com." target = "1 smtp.google.com."
} }
resource "ovh_domain_zone_record" "google_domain_verification_ameliegraf_de" { resource "ovh_domain_zone_record" "google_domain_verification_ameliegraf_de" {
zone = "ameliegraf.de" zone = "ameliegraf.de"
fieldtype = "TXT" fieldtype = "TXT"
ttl = 60 ttl = 60
target = "google-site-verification=LwCbVqOp4c200QYrEEX1m4lIebOzxl5eBb2IkHbrSVA" target = "google-site-verification=LwCbVqOp4c200QYrEEX1m4lIebOzxl5eBb2IkHbrSVA"
} }
resource "ovh_domain_zone_record" "spf_ameliegraf_de" { resource "ovh_domain_zone_record" "spf_ameliegraf_de" {
@@ -121,17 +121,17 @@ resource "ovh_domain_zone_record" "dkim_google_ameliegraf_de" {
resource "ovh_domain_zone_record" "mx_record_graf_fam_de" { resource "ovh_domain_zone_record" "mx_record_graf_fam_de" {
zone = "graf-fam.de" zone = "graf-fam.de"
fieldtype = "MX" fieldtype = "MX"
ttl = 60 ttl = 60
target = "1 smtp.google.com." target = "1 smtp.google.com."
} }
resource "ovh_domain_zone_record" "google_domain_verification_graf_fam_de" { resource "ovh_domain_zone_record" "google_domain_verification_graf_fam_de" {
zone = "graf-fam.de" zone = "graf-fam.de"
fieldtype = "TXT" fieldtype = "TXT"
ttl = 60 ttl = 60
target = "google-site-verification=DcG0tKp5YXxCRvw_Ekxa-3gbzdkZvxXUVkPs8aq9yQ4" target = "google-site-verification=DcG0tKp5YXxCRvw_Ekxa-3gbzdkZvxXUVkPs8aq9yQ4"
} }
resource "ovh_domain_zone_record" "spf_graf_fam_de" { resource "ovh_domain_zone_record" "spf_graf_fam_de" {
@@ -163,17 +163,17 @@ resource "ovh_domain_zone_record" "dkim_google_graf_fam_de" {
resource "ovh_domain_zone_record" "mx_record_krassescheisse_de" { resource "ovh_domain_zone_record" "mx_record_krassescheisse_de" {
zone = "krassescheisse.de" zone = "krassescheisse.de"
fieldtype = "MX" fieldtype = "MX"
ttl = 60 ttl = 60
target = "1 smtp.google.com." target = "1 smtp.google.com."
} }
resource "ovh_domain_zone_record" "google_domain_verification_krassescheisse_de" { resource "ovh_domain_zone_record" "google_domain_verification_krassescheisse_de" {
zone = "krassescheisse.de" zone = "krassescheisse.de"
fieldtype = "TXT" fieldtype = "TXT"
ttl = 60 ttl = 60
target = "google-site-verification=SHFDcMegmOoh9syhj7_xBH9dLQxA2NMnh_zvg4Ma0CM" target = "google-site-verification=SHFDcMegmOoh9syhj7_xBH9dLQxA2NMnh_zvg4Ma0CM"
} }
resource "ovh_domain_zone_record" "spf_krassescheisse_de" { resource "ovh_domain_zone_record" "spf_krassescheisse_de" {
@@ -204,17 +204,17 @@ resource "ovh_domain_zone_record" "dkim_google_krassescheisse_de" {
resource "ovh_domain_zone_record" "mx_record_grabengasse7_de" { resource "ovh_domain_zone_record" "mx_record_grabengasse7_de" {
zone = "grabengasse7.de" zone = "grabengasse7.de"
fieldtype = "MX" fieldtype = "MX"
ttl = 60 ttl = 60
target = "1 smtp.google.com." target = "1 smtp.google.com."
} }
resource "ovh_domain_zone_record" "google_domain_verification_grabengasse7_de" { resource "ovh_domain_zone_record" "google_domain_verification_grabengasse7_de" {
zone = "grabengasse7.de" zone = "grabengasse7.de"
fieldtype = "TXT" fieldtype = "TXT"
ttl = 60 ttl = 60
target = "google-site-verification=zwmmYhET0gyCJr_An22btaOVcpsE_nR2b1St0y4jn1A" target = "google-site-verification=zwmmYhET0gyCJr_An22btaOVcpsE_nR2b1St0y4jn1A"
} }
resource "ovh_domain_zone_record" "spf_grabengasse7_de" { resource "ovh_domain_zone_record" "spf_grabengasse7_de" {
+514
View File
@@ -0,0 +1,514 @@
/* ==========================================================================
HAUMDAUCHER THEME FOR AUTHENTIK
Wurst und Spezialitäten n.e.V.
========================================================================== */
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800;900&display=swap');
/* --- Global Brand Font (Safe for all pages) --- */
:root {
--hd-font: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--pf-global--FontFamily--sans-serif: var(--hd-font);
--pf-v5-global--FontFamily--sans-serif: var(--hd-font);
}
/* ==========================================================================
AUTHENTICATED INTERFACE POLISH (User Dashboard & Admin Console)
Ensure brand logo is well proportioned and high-contrast in masthead
========================================================================== */
ak-interface-user .pf-c-brand img,
ak-interface-admin .pf-c-brand img,
.pf-c-page__header-brand img,
.pf-v5-c-page__header-brand img {
max-height: 38px !important;
width: auto !important;
}
[data-theme='dark'] ak-interface-user .pf-c-brand img,
[data-theme='dark'] ak-interface-admin .pf-c-brand img,
[data-theme='dark'] .pf-c-page__header-brand img,
[data-theme='dark'] .pf-v5-c-page__header-brand img {
filter: invert(1) hue-rotate(180deg);
}
/* ==========================================================================
LOGIN & AUTHENTICATION FLOW SCOPE
All custom card styling, colors, and layout rules are strictly scoped
to ak-flow-executor and .pf-c-login to avoid leaking into the dashboard!
========================================================================== */
/* --- Flow Executor Variables & Overrides --- */
ak-flow-executor,
.pf-c-login,
.pf-v5-c-login,
ak-stage,
.ak-stage {
--hd-bg: #f4f5f8;
--hd-card-bg: #ffffff;
--hd-card-border: rgba(0, 0, 0, 0.08);
--hd-text-main: #111111;
--hd-text-muted: #555555;
--hd-input-bg: #f9f9fb;
--hd-input-border: #d0d5dd;
--hd-btn-bg: #000000;
--hd-btn-text: #ffffff;
--hd-radius: 12px;
--hd-card-radius: 22px;
--hd-transition: all 0.2s ease-in-out;
/* Force PatternFly flow text & inputs to crisp black on the white card */
--pf-global--FontFamily--sans-serif: var(--hd-font) !important;
--pf-v5-global--FontFamily--sans-serif: var(--hd-font) !important;
--pf-global--Color--100: #111111 !important;
--pf-v5-global--Color--100: #111111 !important;
--pf-global--Color--200: #222222 !important;
--pf-v5-global--Color--200: #222222 !important;
--pf-global--Color--300: #444444 !important;
--pf-v5-global--Color--300: #444444 !important;
--pf-global--Color--dark-100: #111111 !important;
--pf-v5-global--Color--dark-100: #111111 !important;
--pf-global--Color--light-100: #111111 !important;
--pf-v5-global--Color--light-100: #111111 !important;
--pf-global--primary-color--100: #000000 !important;
--pf-v5-global--primary-color--100: #000000 !important;
/* Form & Label Variables */
--pf-c-form__label--Color: #111111 !important;
--pf-v5-c-form__label--Color: #111111 !important;
--pf-c-form__label-text--Color: #111111 !important;
--pf-v5-c-form__label-text--Color: #111111 !important;
--pf-c-form-control--Color: #111111 !important;
--pf-v5-c-form-control--Color: #111111 !important;
--pf-c-form-control--placeholder--Color: #666666 !important;
--pf-v5-c-form-control--placeholder--Color: #666666 !important;
--pf-c-form-control--BackgroundColor: #f9f9fb !important;
--pf-v5-c-form-control--BackgroundColor: #f9f9fb !important;
--pf-c-form-control--BorderColor: #d0d5dd !important;
--pf-v5-c-form-control--BorderColor: #d0d5dd !important;
--pf-c-form__helper-text--Color: #555555 !important;
--pf-v5-c-form__helper-text--Color: #555555 !important;
}
/* --- Flow Executor Viewport Centering & Background --- */
ak-flow-executor,
.pf-c-login,
.pf-v5-c-login {
min-height: 100vh !important;
min-height: 100dvh !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
background-color: #f4f5f8 !important;
background-image: radial-gradient(circle at 50% 15%, rgba(0, 0, 0, 0.03) 0%, transparent 60%) !important;
background-attachment: fixed !important;
background-size: cover !important;
padding: 32px 16px 48px 16px !important;
box-sizing: border-box !important;
position: relative !important;
font-family: var(--hd-font) !important;
color: #111111 !important;
}
/* --- Centered Login Container --- */
ak-flow-executor .pf-c-login__container,
.pf-c-login .pf-c-login__container,
.pf-v5-c-login .pf-v5-c-login__container {
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
width: 100% !important;
max-width: 420px !important;
margin: auto !important;
padding: 0 !important;
box-sizing: border-box !important;
}
/* --- Top Header & Logo Layout --- */
ak-flow-executor .pf-c-login__header,
.pf-c-login .pf-c-login__header,
.pf-v5-c-login .pf-v5-c-login__header {
display: flex !important;
justify-content: center !important;
align-items: center !important;
width: 100% !important;
margin-top: 0 !important;
margin-bottom: 24px !important;
padding: 0 !important;
}
ak-flow-executor .pf-c-login__header ak-brand,
ak-flow-executor .pf-c-login__header img,
.pf-c-login .pf-c-login__header ak-brand,
.pf-c-login .pf-c-login__header img,
.pf-v5-c-login .pf-v5-c-login__header ak-brand,
.pf-v5-c-login .pf-v5-c-login__header img {
display: block !important;
max-height: 85px !important;
max-width: 240px !important;
width: auto !important;
margin: 0 auto !important;
mix-blend-mode: multiply !important;
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.08)) !important;
transition: all 0.2s ease-in-out;
}
/* --- Classic White Card --- */
ak-flow-executor .pf-c-login__main,
.pf-c-login .pf-c-login__main,
.pf-v5-c-login .pf-v5-c-login__main,
ak-flow-executor ak-stage,
ak-flow-executor .ak-stage {
background: #ffffff !important;
border: 1px solid rgba(0, 0, 0, 0.08) !important;
border-radius: 22px !important;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.06), 0 1px 3px rgba(0, 0, 0, 0.04) !important;
padding: 36px 32px 32px 32px !important;
width: 100% !important;
box-sizing: border-box !important;
color: #111111 !important;
}
/* --- Fully Hide Duplicate / Generic Flow Title --- */
ak-flow-executor .pf-c-login__main-header-title,
ak-flow-executor .pf-v5-c-login__main-header-title,
ak-flow-executor .pf-c-title,
ak-flow-executor .pf-v5-c-title,
.pf-c-login .pf-c-login__main-header-title,
.pf-v5-c-login .pf-v5-c-login__main-header-title {
display: none !important;
}
ak-flow-executor .pf-c-login__main-header,
.pf-c-login .pf-c-login__main-header,
.pf-v5-c-login .pf-v5-c-login__main-header {
margin-bottom: 24px !important;
padding: 0 !important;
text-align: center !important;
}
ak-flow-executor .pf-c-login__main-header-desc,
.pf-c-login .pf-c-login__main-header-desc,
.pf-v5-c-login .pf-v5-c-login__main-header-desc {
font-family: var(--hd-font) !important;
font-size: 0.95rem !important;
line-height: 1.5 !important;
margin: 0 !important;
color: #555555 !important;
text-align: center !important;
}
ak-flow-executor .pf-c-login__main-header-desc strong,
ak-flow-executor .pf-c-login__main-header-desc b,
.pf-c-login .pf-c-login__main-header-desc strong,
.pf-c-login .pf-c-login__main-header-desc b,
.pf-v5-c-login .pf-v5-c-login__main-header-desc strong,
.pf-v5-c-login .pf-v5-c-login__main-header-desc b {
color: #111111 !important;
font-weight: 700 !important;
}
/* --- Form Field Groups --- */
ak-flow-executor .pf-c-form__group,
.pf-c-login .pf-c-form__group,
.pf-v5-c-login .pf-v5-c-form__group {
margin-bottom: 20px !important;
}
/* --- LABELS & LABEL TEXT (High-Contrast Black inside login flow) --- */
ak-flow-executor .pf-c-form__label,
ak-flow-executor .pf-v5-c-form__label,
ak-flow-executor .pf-c-form__label-text,
ak-flow-executor .pf-v5-c-form__label-text,
ak-flow-executor .pf-c-form__group-label,
ak-flow-executor .pf-v5-c-form__group-label,
ak-flow-executor label,
ak-flow-executor label *,
.pf-c-login .pf-c-form__label,
.pf-c-login .pf-v5-c-form__label,
.pf-c-login .pf-c-form__label-text,
.pf-c-login .pf-v5-c-form__label-text,
.pf-c-login label,
.pf-c-login label *,
.pf-v5-c-login label,
.pf-v5-c-login label * {
font-family: var(--hd-font) !important;
font-weight: 600 !important;
font-size: 0.9rem !important;
color: #111111 !important;
margin-bottom: 6px !important;
display: inline-block !important;
opacity: 1 !important;
}
/* Required Asterisk */
ak-flow-executor .pf-c-form__label-required,
ak-flow-executor .pf-v5-c-form__label-required,
.pf-c-login .pf-c-form__label-required,
.pf-v5-c-login .pf-v5-c-form__label-required {
color: #c9190b !important;
margin-left: 4px !important;
font-weight: 700 !important;
}
/* --- INPUT FIELDS & TEXT (inside login flow) --- */
ak-flow-executor .pf-c-form-control,
ak-flow-executor .pf-v5-c-form-control,
ak-flow-executor input[type="text"],
ak-flow-executor input[type="password"],
ak-flow-executor input[type="email"],
ak-flow-executor input,
.pf-c-login .pf-c-form-control,
.pf-c-login .pf-v5-c-form-control,
.pf-c-login input,
.pf-v5-c-login input {
font-family: var(--hd-font) !important;
font-size: 0.98rem !important;
border-radius: 12px !important;
padding: 12px 14px !important;
background-color: #f9f9fb !important;
border: 1px solid #d0d5dd !important;
color: #111111 !important;
transition: all 0.2s ease-in-out !important;
width: 100% !important;
box-sizing: border-box !important;
}
/* Input Placeholder */
ak-flow-executor input::placeholder,
.pf-c-login input::placeholder,
.pf-v5-c-login input::placeholder {
color: #666666 !important;
font-style: normal !important;
opacity: 1 !important;
}
/* Focus State */
ak-flow-executor input:focus,
.pf-c-login input:focus,
.pf-v5-c-login input:focus {
outline: none !important;
border-color: #000000 !important;
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.12) !important;
background-color: #ffffff !important;
color: #111111 !important;
}
/* Browser Autofill Fix (Prevent White on White) */
ak-flow-executor input:-webkit-autofill,
ak-flow-executor input:-webkit-autofill:hover,
ak-flow-executor input:-webkit-autofill:focus,
.pf-c-login input:-webkit-autofill,
.pf-v5-c-login input:-webkit-autofill {
-webkit-text-fill-color: #111111 !important;
-webkit-box-shadow: 0 0 0 1000px #f9f9fb inset !important;
box-shadow: 0 0 0 1000px #f9f9fb inset !important;
transition: background-color 5000s ease-in-out 0s !important;
}
/* --- Solid Black Submit Button (inside login flow) --- */
ak-flow-executor button[type="submit"],
ak-flow-executor .pf-c-button.pf-m-primary,
ak-flow-executor .pf-v5-c-button.pf-m-primary,
.pf-c-login button[type="submit"],
.pf-c-login .pf-c-button.pf-m-primary,
.pf-v5-c-login button[type="submit"],
.pf-v5-c-login .pf-v5-c-button.pf-m-primary {
font-family: var(--hd-font) !important;
font-size: 1rem !important;
font-weight: 700 !important;
letter-spacing: 0.3px !important;
border-radius: 12px !important;
padding: 13px 20px !important;
border: none !important;
cursor: pointer !important;
background: #000000 !important;
color: #ffffff !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
transition: all 0.2s ease-in-out !important;
width: 100% !important;
margin-top: 16px !important;
margin-bottom: 4px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
ak-flow-executor button[type="submit"]:hover,
.pf-c-login button[type="submit"]:hover,
.pf-v5-c-login button[type="submit"]:hover {
background: #222222 !important;
color: #ffffff !important;
transform: translateY(-1px) !important;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2) !important;
}
ak-flow-executor button[type="submit"]:active,
.pf-c-login button[type="submit"]:active,
.pf-v5-c-login button[type="submit"]:active {
transform: translateY(0) !important;
}
/* Secondary Buttons (inside login flow) */
ak-flow-executor .pf-c-button.pf-m-secondary,
ak-flow-executor .pf-v5-c-button.pf-m-secondary,
.pf-c-login .pf-c-button.pf-m-secondary,
.pf-v5-c-login .pf-v5-c-button.pf-m-secondary {
font-family: var(--hd-font) !important;
font-weight: 600 !important;
border-radius: 12px !important;
padding: 11px 18px !important;
border: 1px solid #d0d5dd !important;
background: #ffffff !important;
color: #111111 !important;
transition: all 0.2s ease-in-out !important;
}
ak-flow-executor .pf-c-button.pf-m-secondary:hover,
.pf-c-login .pf-c-button.pf-m-secondary:hover,
.pf-v5-c-login .pf-v5-c-button.pf-m-secondary:hover {
background: #f4f5f7 !important;
border-color: #000000 !important;
}
/* --- Alerts & Messages (inside login flow) --- */
ak-flow-executor .pf-c-alert,
ak-flow-executor .pf-v5-c-alert,
.pf-c-login .pf-c-alert,
.pf-v5-c-login .pf-v5-c-alert {
border-radius: 12px !important;
margin-bottom: 16px !important;
}
/* --- LANGUAGE BUTTON AT BOTTOM (inside login flow only) --- */
ak-flow-executor ak-locale-select,
ak-flow-executor [part="locale-select"],
ak-flow-executor::part(locale-select),
.pf-c-login ak-locale-select,
.pf-v5-c-login ak-locale-select {
position: fixed !important;
inset-block-start: auto !important;
inset-inline-start: auto !important;
top: auto !important;
left: auto !important;
bottom: 14px !important;
right: 18px !important;
z-index: 999 !important;
opacity: 0.65 !important;
transition: opacity 0.2s ease, transform 0.2s ease !important;
}
ak-flow-executor ak-locale-select:hover,
.pf-c-login ak-locale-select:hover {
opacity: 1 !important;
transform: translateY(-1px) !important;
}
ak-flow-executor ak-locale-select select,
.pf-c-login ak-locale-select select {
color: #111111 !important;
background: transparent !important;
cursor: pointer !important;
}
ak-flow-executor ak-locale-select label,
.pf-c-login ak-locale-select label {
color: #111111 !important;
cursor: pointer !important;
}
/* --- Footer & Copyright (inside login flow) --- */
ak-flow-executor ak-brand-links,
ak-flow-executor .pf-c-login__footer,
ak-flow-executor .pf-v5-c-login__footer,
.pf-c-login .pf-c-login__footer,
.pf-v5-c-login .pf-v5-c-login__footer {
margin-top: 24px !important;
margin-bottom: 0 !important;
text-align: center !important;
font-size: 0.82rem !important;
color: #555555 !important;
}
ak-flow-executor ak-brand-links a,
ak-flow-executor .pf-c-login__footer a,
.pf-c-login .pf-c-login__footer a,
.pf-v5-c-login .pf-v5-c-login__footer a {
color: #111111 !important;
text-decoration: none !important;
font-weight: 600 !important;
}
ak-flow-executor ak-brand-links a:hover,
ak-flow-executor .pf-c-login__footer a:hover,
.pf-c-login .pf-c-login__footer a:hover,
.pf-v5-c-login .pf-v5-c-login__footer a:hover {
text-decoration: underline !important;
}
/* --- Watermark (inside login flow container) --- */
ak-flow-executor .pf-c-login__container::after,
.pf-c-login .pf-c-login__container::after,
.pf-v5-c-login .pf-v5-c-login__container::after {
content: "HAUMDAUCHER • Wurst und Spezialitäten n.e.V.";
display: block;
margin-top: 28px;
margin-bottom: 12px;
font-size: 0.76rem;
letter-spacing: 1.5px;
text-transform: uppercase;
font-weight: 600;
color: #888888;
text-align: center;
}
/* --- Mobile Responsiveness (inside login flow) --- */
@media (max-width: 480px) {
ak-flow-executor,
.pf-c-login,
.pf-v5-c-login {
padding: 24px 12px 48px 12px !important;
}
ak-flow-executor .pf-c-login__container,
.pf-c-login .pf-c-login__container,
.pf-v5-c-login .pf-v5-c-login__container {
padding: 0 !important;
}
ak-flow-executor .pf-c-login__header,
.pf-c-login .pf-c-login__header,
.pf-v5-c-login .pf-v5-c-login__header {
margin-bottom: 18px !important;
}
ak-flow-executor .pf-c-login__main,
.pf-c-login .pf-c-login__main,
.pf-v5-c-login .pf-v5-c-login__main,
ak-flow-executor ak-stage,
ak-flow-executor .ak-stage {
padding: 28px 20px 24px 20px !important;
border-radius: 18px !important;
}
ak-flow-executor .pf-c-login__header img,
.pf-c-login .pf-c-login__header img,
.pf-v5-c-login .pf-v5-c-login__header img {
max-height: 72px !important;
}
ak-flow-executor .pf-c-login__container::after,
.pf-c-login .pf-c-login__container::after,
.pf-v5-c-login .pf-v5-c-login__container::after {
margin-top: 20px;
margin-bottom: 8px;
}
ak-flow-executor ak-locale-select,
.pf-c-login ak-locale-select,
.pf-v5-c-login ak-locale-select {
bottom: 10px !important;
right: 12px !important;
}
}
+100 -80
View File
@@ -6,7 +6,7 @@ provider "ovh" {
} }
locals { locals {
ttl = "60" ttl = "60"
} }
## old moritzgrafde ## old moritzgrafde
@@ -15,65 +15,65 @@ locals {
## => 91.121.64.43 ## => 91.121.64.43
variable "subdomains_moritzgraf" { variable "subdomains_moritzgraf" {
default = ["auth","prometheus","alertmanager","chat","mqtt", "cloud", default = ["auth", "prometheus", "alertmanager", "chat", "mqtt", "cloud",
"monitoring","smtp","pop3","imap","jaeger","ldap","code","hub", "monitoring", "smtp", "pop3", "imap", "jaeger", "ldap", "code", "hub",
"grafana","phpmyadmin","webapp","kc","phppgadmin","www","rstudio","code" ] "grafana", "phpmyadmin", "webapp", "kc", "phppgadmin", "www", "rstudio", "code"]
} }
resource "ovh_domain_zone_record" "moritzgraf_subdomains" { resource "ovh_domain_zone_record" "moritzgraf_subdomains" {
for_each = toset( var.subdomains_moritzgraf ) for_each = toset(var.subdomains_moritzgraf)
zone = "moritzgraf.de" zone = "moritzgraf.de"
subdomain = each.key subdomain = each.key
fieldtype = "CNAME" fieldtype = "CNAME"
ttl = local.ttl ttl = local.ttl
target = "moritzgraf.de." target = "moritzgraf.de."
} }
## new haumdaucher k8s ## new haumdaucher k8s
variable "subdomains_moritzgraf_k8s" { variable "subdomains_moritzgraf_k8s" {
default = ["corona","corona-api","tt-rss","git" ] default = ["corona", "corona-api", "tt-rss", "git"]
} }
resource "ovh_domain_zone_record" "moritzgraf_subdomains_k8s" { resource "ovh_domain_zone_record" "moritzgraf_subdomains_k8s" {
for_each = toset( var.subdomains_moritzgraf_k8s ) for_each = toset(var.subdomains_moritzgraf_k8s)
zone = "moritzgraf.de" zone = "moritzgraf.de"
subdomain = each.key subdomain = each.key
fieldtype = "CNAME" fieldtype = "CNAME"
ttl = local.ttl ttl = local.ttl
target = "haumdaucher.de." target = "haumdaucher.de."
} }
# Kochen # Kochen
resource "ovh_domain_zone_record" "kochen_haumdaucher" { resource "ovh_domain_zone_record" "kochen_haumdaucher" {
zone = "haumdaucher.de" zone = "haumdaucher.de"
subdomain = "kochen" subdomain = "kochen"
fieldtype = "CNAME" fieldtype = "CNAME"
ttl = local.ttl ttl = local.ttl
target = "haumdaucher-datalab.github.io." target = "haumdaucher-datalab.github.io."
} }
## nodes ## nodes
resource "ovh_domain_zone_record" "one" { resource "ovh_domain_zone_record" "one" {
zone = "haumdaucher.de" zone = "haumdaucher.de"
subdomain = "one" subdomain = "one"
fieldtype = "A" fieldtype = "A"
ttl = local.ttl ttl = local.ttl
#target = "91.121.84.190" #target = "91.121.84.190"
target = "136.243.23.215" target = "136.243.23.215"
} }
## krassescheisse ## krassescheisse
resource "ovh_domain_zone_record" "krassescheisse_subdomains" { resource "ovh_domain_zone_record" "krassescheisse_subdomains" {
for_each = toset( var.subdomains_moritzgraf ) for_each = toset(var.subdomains_moritzgraf)
zone = "krassescheisse.de" zone = "krassescheisse.de"
subdomain = each.key subdomain = each.key
fieldtype = "CNAME" fieldtype = "CNAME"
ttl = local.ttl ttl = local.ttl
target = "krassescheisse.de." target = "krassescheisse.de."
} }
@@ -92,36 +92,36 @@ variable "subdomains_haumdaucher" {
# ## graf.me -> problems with domain # ## graf.me -> problems with domain
variable "domains" { variable "domains" {
default = [ "moritzgraf.de" default = ["moritzgraf.de"
,"graf-fam.de" , "graf-fam.de"
,"krassescheisse.de"] , "krassescheisse.de"]
} }
resource "ovh_domain_zone_record" "haumdaucher_domain" { resource "ovh_domain_zone_record" "haumdaucher_domain" {
zone = "haumdaucher.de" zone = "haumdaucher.de"
subdomain = "" subdomain = ""
fieldtype = "A" fieldtype = "A"
ttl = local.ttl ttl = local.ttl
#target = "91.121.84.190" #target = "91.121.84.190"
target = "136.243.23.215" target = "136.243.23.215"
} }
resource "ovh_domain_zone_record" "haumdaucher_wildcard_domain" { resource "ovh_domain_zone_record" "haumdaucher_wildcard_domain" {
zone = "haumdaucher.de" zone = "haumdaucher.de"
subdomain = "*" subdomain = "*"
fieldtype = "A" fieldtype = "A"
ttl = local.ttl ttl = local.ttl
#target = "91.121.84.190" #target = "91.121.84.190"
target = "136.243.23.215" target = "136.243.23.215"
} }
resource "ovh_domain_zone_record" "moritzgraf_wildcard_domain" { resource "ovh_domain_zone_record" "moritzgraf_wildcard_domain" {
zone = "moritzgraf.de" zone = "moritzgraf.de"
subdomain = "*" subdomain = "*"
fieldtype = "A" fieldtype = "A"
ttl = local.ttl ttl = local.ttl
#target = "91.121.84.190" #target = "91.121.84.190"
target = "136.243.23.215" target = "136.243.23.215"
} }
###################################### ######################################
@@ -130,40 +130,60 @@ resource "ovh_domain_zone_record" "moritzgraf_wildcard_domain" {
locals { locals {
# adobe # adobe
first = "151.101.128.119" first = "151.101.128.119"
second = "151.101.192.119" second = "151.101.192.119"
} }
resource "ovh_domain_zone_record" "ameliegraf_de_first" { resource "ovh_domain_zone_record" "ameliegraf_de_first" {
zone = "ameliegraf.de" zone = "ameliegraf.de"
subdomain = "" subdomain = ""
fieldtype = "A" fieldtype = "A"
ttl = local.ttl ttl = local.ttl
#target = "91.121.84.190" #target = "91.121.84.190"
target = local.first target = local.first
} }
resource "ovh_domain_zone_record" "ameliegraf_de_second" { resource "ovh_domain_zone_record" "ameliegraf_de_second" {
zone = "ameliegraf.de" zone = "ameliegraf.de"
subdomain = "" subdomain = ""
fieldtype = "A" fieldtype = "A"
ttl = local.ttl ttl = local.ttl
#target = "91.121.84.190" #target = "91.121.84.190"
target = local.second target = local.second
} }
resource "ovh_domain_zone_record" "wildcard_ameliegraf_de_first" { resource "ovh_domain_zone_record" "wildcard_ameliegraf_de_first" {
zone = "ameliegraf.de" zone = "ameliegraf.de"
subdomain = "*" subdomain = "*"
fieldtype = "A" fieldtype = "A"
ttl = local.ttl ttl = local.ttl
#target = "91.121.84.190" #target = "91.121.84.190"
target = local.first target = local.first
} }
resource "ovh_domain_zone_record" "wildcard_ameliegraf_de_second" { resource "ovh_domain_zone_record" "wildcard_ameliegraf_de_second" {
zone = "ameliegraf.de" zone = "ameliegraf.de"
subdomain = "*" subdomain = "*"
fieldtype = "A" fieldtype = "A"
ttl = local.ttl ttl = local.ttl
#target = "91.121.84.190" #target = "91.121.84.190"
target = local.second target = local.second
} }
######################################
## grabengasse7.de
resource "ovh_domain_zone_record" "grabengasse7_de_domain" {
zone = "grabengasse7.de"
subdomain = ""
fieldtype = "A"
ttl = local.ttl
target = "136.243.23.215"
}
resource "ovh_domain_zone_record" "grabengasse7_de_wildcard" {
zone = "grabengasse7.de"
subdomain = "*"
fieldtype = "A"
ttl = local.ttl
target = "136.243.23.215"
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4
View File
@@ -11,6 +11,10 @@ terraform {
source = "hashicorp/local" source = "hashicorp/local"
version = "~> 2.0" version = "~> 2.0"
} }
authentik = {
source = "goauthentik/authentik"
version = ">= 2024.12"
}
} }
required_version = ">= 0.13" required_version = ">= 0.13"
} }
+47 -13
View File
@@ -10,26 +10,59 @@ This directory manages the configuration, code templating, and deployment for th
* **[scripts/Taupi-4.0.templated.js](file:///Users/moritz/src/infrapuzzle/terraform_shelly/scripts/Taupi-4.0.templated.js)**: The locally generated script rendering the variables passed from Terraform. Do not edit directly. * **[scripts/Taupi-4.0.templated.js](file:///Users/moritz/src/infrapuzzle/terraform_shelly/scripts/Taupi-4.0.templated.js)**: The locally generated script rendering the variables passed from Terraform. Do not edit directly.
* **[scripts/upload.py](file:///Users/moritz/src/infrapuzzle/terraform_shelly/scripts/upload.py)**: Helper script executing during `terraform apply` to upload the templated script to the Shelly device over local HTTP RPC commands. * **[scripts/upload.py](file:///Users/moritz/src/infrapuzzle/terraform_shelly/scripts/upload.py)**: Helper script executing during `terraform apply` to upload the templated script to the Shelly device over local HTTP RPC commands.
## Architecture Decisions ## Architecture Decisions & Logic Gates
### 1. Linear Isopleth Approximation (LIM I) The ventilation controller uses a **modular logic-gate architecture** designed to keep the cellar dry while preventing cellar overheating in summer and overcooling in winter.
To determine if indoor relative humidity is high enough to warrant ventilation, we use a linear approximation of the **Lowest Isopleth for Mould (LIM I)** curve developed by Klaus Sedlbauer for biodegradable substrates (like wood or wallpaper).
Between $5^\circ\text{C}$ and $25^\circ\text{C}$, the critical relative humidity threshold is calculated dynamically based on room temperature ($T$): ### 1. Decoupled Mold Alert (Sedlbauer LIM I)
To distinguish between **routine/proactive drying** and **genuine mold risk**, the mold alert (`taupi_is_critical` metric $\rightarrow$ Prometheus alert `TaupiFanMoldDanger`) is decoupled from the fan start threshold:
* **LIM I Baseline**: $RH_{crit}(T) = 80\% - 0.5 \cdot (T - 10)$
* **Alert Activation**: $RH_{in} \ge RH_{crit}(T) - \text{critical\_humi\_buffer}$
* **Behavior**: Fires an alert only when humidity poses an actual germination hazard on cold outer walls.
$$RH_{crit}(T) = 80\% - 0.5 \cdot (T - 10)$$ ### 2. Cellar Overheating Prevention
In warm months, blowing outdoor air into a cool cellar can heat up the cellar mass.
* **Ceiling Protection**: If $T_{in} \ge 20^\circ\text{C}$, the fan is **only** permitted to run if outdoor air is cooler than inside ($T_{out} \le T_{in}$).
* **Warming Allowance**: If $T_{in} < 20^\circ\text{C}$, gentle warming is permitted, but outdoor temperature cannot exceed $20^\circ\text{C}$ ($T_{out} \le 20^\circ\text{C}$).
* At $10^\circ\text{C}$, the critical threshold is $80\%$. ### 3. Seasonal Target Humidities
* At $20^\circ\text{C}$, the critical threshold is $75\%$. * **Cold / Winter Mode** ($T_{in} < 14^\circ\text{C}$ or $T_{out} < 10^\circ\text{C}$): Targets **50% RH**. Once 50% RH is reached, the fan stops to preserve cellar warmth.
* **Warm / Summer Mode**: Targets **40% RH** for maximum drying.
* **Hard Cold Floor**: The fan is blocked if $T_{in} \le \text{mindesttemperatur}$ (default 10°C).
### 2. Room-to-Wall Safety Buffer (15%) ### 4. 2-Tier Scheduling & Optimal Night Window
Since the sensor measures ambient room air, but mold grows on colder outer wall surfaces, we must compensate for the thermal gradient. In Central Europe / Germany, the diurnal temperature and absolute humidity minimum occurs in early morning (~03:00–07:00, peak around 05:00):
* **Physical Effect**: A $5^\circ\text{C}$ drop in temperature between room air ($20^\circ\text{C}$) and the wall surface ($15^\circ\text{C}$) causes the local relative humidity at the wall to spike from $60\%$ to $82\%$, crossing the mold germination threshold. * **Urgent Drying ($RH_{in} > 60\%$)**: Runs 24/7 whenever psychrometric and temperature guards allow.
* **Action**: We apply a **$15\%$ safety buffer** to the threshold. This targets keeping the ambient room air relative humidity below **$60\%$** (when the room is at $20^\circ\text{C}$) to keep wall surfaces safely below $80\%$ relative humidity. * **Routine Drying ($40\% \le RH_{in} \le 60\%$)**: Restricted to the **optimal night window (03:00–07:00)** to exploit maximum dew point spread.
The fan is only allowed to run if: ### 5. Duty Cycle & Thermal Equilibrium
To allow cellar air and thermal wall mass to equilibrate and prevent non-stop continuous blowing:
* **Max Continuous Runtime**: 90 minutes.
* **Cooldown Rest**: 45 minutes pause after hitting max continuous runtime.
$$\text{humidity\_innen} \ge RH_{crit}(T) - 15\%$$ ---
## Logic Gate Summary
```
[ Telemetry Fresh & Valid? ]
│ YES
[ Cooldown Rest Elapsed? ]
│ YES
[ Cellar Temp > Min Floor (10°C)? ]
│ YES
[ Overheating Safe (Tout vs Tin)? ]
│ YES
[ Cellar RH > Target (40% / 50%)? ]
│ YES
[ Dew Point Spread > Threshold (2°C)? ]
│ YES
[ Urgent (>60%) OR Night Window (03-07)? ]
│ YES
┌────────┴────────┐
▼ ▼
[ START BLOWER ] [ LOG REASON ]
```
--- ---
@@ -38,3 +71,4 @@ $$\text{humidity\_innen} \ge RH_{crit}(T) - 15\%$$
* **[BoeserBob/Taupi-4.0](https://github.com/BoeserBob/Taupi-4.0)**: The original upstream Shelly dew point ventilation controller repository. * **[BoeserBob/Taupi-4.0](https://github.com/BoeserBob/Taupi-4.0)**: The original upstream Shelly dew point ventilation controller repository.
* **[Klaus Sedlbauer Biography (Wikipedia)](https://de.wikipedia.org/wiki/Klaus_Sedlbauer)**: The researcher who developed the Lowest Isopleth for Mould (LIM) curves at Fraunhofer IBP. * **[Klaus Sedlbauer Biography (Wikipedia)](https://de.wikipedia.org/wiki/Klaus_Sedlbauer)**: The researcher who developed the Lowest Isopleth for Mould (LIM) curves at Fraunhofer IBP.
* **[WUFI Biohygrothermal Model](https://wufi.de/en/wufi-software/wufi-bio/)**: Fraunhofer IBP documentation explaining dynamic mold growth and spore germination models. * **[WUFI Biohygrothermal Model](https://wufi.de/en/wufi-software/wufi-bio/)**: Fraunhofer IBP documentation explaining dynamic mold growth and spore germination models.
+10 -1
View File
@@ -4,7 +4,16 @@ locals {
sensor_innen_mac = var.sensor_innen_mac sensor_innen_mac = var.sensor_innen_mac
taupunktschwelle = var.taupunktschwelle taupunktschwelle = var.taupunktschwelle
mindesttemperatur = var.mindesttemperatur mindesttemperatur = var.mindesttemperatur
mindesthumi = var.mindesthumi max_indoor_temp_heating = var.max_indoor_temp_heating
winter_temp_threshold_in = var.winter_temp_threshold_in
winter_temp_threshold_out = var.winter_temp_threshold_out
target_humi_summer = var.target_humi_summer
target_humi_winter = var.target_humi_winter
urgent_humi_threshold = var.urgent_humi_threshold
night_window_start_hour = var.night_window_start_hour
night_window_end_hour = var.night_window_end_hour
max_runtime_minutes = var.max_runtime_minutes
min_pause_minutes = var.min_pause_minutes
schaltzeit = var.schaltzeit schaltzeit = var.schaltzeit
battery_warngrenze = var.battery_warngrenze battery_warngrenze = var.battery_warngrenze
lost_connection = var.lost_connection lost_connection = var.lost_connection
+261 -360
View File
@@ -1,398 +1,299 @@
////////////// TAUPI 4.0 @ Shelly ////////////// ////////////// TAUPI 4.5 @ Shelly //////////////
// copyright by boeserbob und holzachr // Dynamic Quality-Score Psychrometric Controller & Adaptive Thermal Governor
// Fragen an quirb@web.de
// Dokumentation und aktuelle Versionen unter https://github.com/BoeserBob/Taupi-4.0
//
// Dieses Skript verwandelt z.B. eine Shelly Plug in eine Taupunktlüftersteuerung.
// Der Skript schaltet einen angeschlossenen Lüfter über den Schalter des Shellys auf dem er installiert ist entsprechend der Taupunktunterschiede innen - außen.
// - Es empfängt Messwert-Events von BLE-Sensoren auf.
// - Wenn die Messwerte von den angegebenen Innen- und Außen-Sensoren stammen, werden aus Temperatur und Luftfeuchte die jeweiligen Taupunkte berechnet.
// - Eine Timerschleife überprüft regelmäßig, ob alle Einschaltbedingungen fuer den Lüfter erfüllt sind:
// - Wenn der Taupunkt innen größer als der Taupunkt außen + einem Schwellwert ist wird der Lüfter eingeschaltet, sonst ausgeschaltet.
// - Wenn die Innentermperatur unter 10 °C und die Innenraumfeuchte unter 50 % ist wird der Lüfer ausgeschaltet.
//========== Sensor-Konfiguration ========== var MAC_OUT = "${sensor_aussen_mac}".toLowerCase();
var sensor_aussen = "${sensor_aussen_mac}"; var MAC_IN = "${sensor_innen_mac}".toLowerCase();
var sensor_innen = "${sensor_innen_mac}";
//========== Debug-Konfiguration ==========
var DEBUG = false;
//========== Schalt-Konfiguration ==========
var taupunktschwelle = ${taupunktschwelle}; // [°C] Lüfter einschalten wenn TPinnen > (TPaussen + taupunktschwelle)...
var mindesttemperatur = ${mindesttemperatur}; // [°C] ...und Tinnen > mindesttemperatur...
var mindesthumi = ${mindesthumi}; // [%] ...und RHinnen > mindesthumi
var schaltzeit = ${schaltzeit}; // [s] Schaltbedingung prüfen alle X Sekunden
var battery_warngrenze = ${battery_warngrenze}; // [%] wenn dieser Schwellwert unterschritten ist blinkt der Plug rot
var lost_connection = ${lost_connection}; // [s] Zeit nach der frische Sensordaten gekommen sein müssen um tote Verbindungen zu finden
//========== Kritische Feuchte-Konfiguration (Schimmelgefahr) ==========
var critical_humi_base = ${critical_humi_base}; // [%] Kritische relative Feuchte bei Referenztemp.
var critical_humi_temp_coeff = ${critical_humi_temp_coeff}; // [%/°C] Steigung des Grenzwerts pro °C
var critical_humi_ref_temp = ${critical_humi_ref_temp}; // [°C] Referenztemperatur
var critical_humi_buffer = ${critical_humi_buffer}; // [%] Sicherheitspuffer
//===== Ende Sensor-Konfiguration === AB HIER MUSS NICHTS MEHR GEÄNDERT WERDEN =====================================
var taupunkt_aussen; var SCHALTZEIT_SEC = ${schaltzeit};
var taupunkt_innen; var LOST_LIMIT = ${lost_connection};
var temperatur_innen; var BATT_LIMIT = ${battery_warngrenze};
var temperatur_aussen; var DP_SPREAD_BASE = ${taupunktschwelle};
var humidity_innen; var MIN_TEMP_FLOOR = ${mindesttemperatur};
var humidity_aussen; var MAX_HEAT_CEIL = ${max_indoor_temp_heating};
var battery_innen; var WINTER_IN_TH = ${winter_temp_threshold_in};
var battery_aussen; var TARGET_SUMMER = ${target_humi_summer};
var lost_connection_innen; var TARGET_WINTER = ${target_humi_winter};
var lost_connection_aussen; var MIN_PAUSE_MIN = ${min_pause_minutes};
var luefterstatus = null; // Merkt sich letzten Schaltzustand, um unnötige Schaltvorgänge zu vermeiden var LIM_BASE = ${critical_humi_base};
var LIM_COEFF = ${critical_humi_temp_coeff};
var LIM_REF_T = ${critical_humi_ref_temp};
var LIM_BUF = ${critical_humi_buffer};
// Taupunktberechnung var ti = undefined, hi = undefined, di = undefined, bi = undefined, age_in = 999999;
function taupunkt(T, RH) { var to = undefined, ho = undefined, do_ = undefined, bo = undefined, age_out = 999999;
var a = (T >= 0) ? 17.27 : 21.875;
var b = (T >= 0) ? 237.7 : 265.5; var fan_run_sec = 0, fan_pause_sec = 0, relay_on = false, last_reason = "INIT", last_color = -1;
var st_run = 0, st_target = TARGET_SUMMER, st_mold_th = 70.0, st_mold_crit = 0, st_spread = 0;
var st_quality = 0, st_max_run_min = 0, st_gov_state = 0;
var pid_out = -1, pid_in = -1;
function calcDewPoint(T, RH) {
if (typeof T !== "number" || typeof RH !== "number" || isNaN(T) || isNaN(RH) || RH <= 0) return undefined;
var a = (T >= 0) ? 17.27 : 21.875, b = (T >= 0) ? 237.7 : 265.5;
var alpha = (a * T) / (b + T) + Math.log(RH / 100); var alpha = (a * T) / (b + T) + Math.log(RH / 100);
return (b * alpha) / (a - alpha); return (b * alpha) / (a - alpha);
} }
// Lüftersteuerung function calcMoldThresh(T) {
function schalten() { if (typeof T !== "number" || isNaN(T)) return undefined;
// Sicherheitsprüfung: Sind alle benötigten Werte vorhanden? return LIM_BASE - LIM_COEFF * (T - LIM_REF_T) - LIM_BUF;
if (typeof taupunkt_innen === "undefined" || }
typeof taupunkt_aussen === "undefined" ||
typeof temperatur_innen === "undefined" || function evaluateDecision() {
typeof humidity_innen === "undefined") var telOk = (typeof ti === "number" && typeof to === "number" && typeof hi === "number" && typeof ho === "number" &&
{ typeof di === "number" && typeof do_ === "number" && !isNaN(ti) && !isNaN(to) && !isNaN(hi) &&
print("Nicht alle Sensorwerte vorhanden - Schaltung übersprungen."); !isNaN(ho) && !isNaN(di) && !isNaN(do_) && age_in <= LOST_LIMIT && age_out <= LOST_LIMIT);
farbring(80,80,0,100); if (!telOk) return { run: false, reason: "TELEMETRY_INVALID", maxRun: 0, qual: 0, gov: 0 };
return;
if (fan_pause_sec > 0) return { run: false, reason: "COOLDOWN_PAUSE", maxRun: 0, qual: 0, gov: 0 };
var mt = calcMoldThresh(ti);
var isMoldCrit = (typeof mt === "number" && hi >= mt);
// 1. Frost Floor Check
if (ti <= MIN_TEMP_FLOOR && !isMoldCrit) {
return { run: false, reason: "FROST_PROTECTION", maxRun: 0, qual: 0, gov: 1 };
} }
// Kritische Feuchte (Schimmelgrenzkurve) berechnen // 2. Adaptive Thermal Governor
var rh_crit = critical_humi_base - critical_humi_temp_coeff * (temperatur_innen - critical_humi_ref_temp); var reqSpread = DP_SPREAD_BASE;
var rh_activation = rh_crit - critical_humi_buffer; var targetHumi = TARGET_SUMMER;
var is_critical = humidity_innen >= rh_activation; var govState = 2; // Comfort default
print("Schimmelprüfung: T_innen =", temperatur_innen, "°C, RH_innen =", humidity_innen, "%, RH_crit =", rh_crit, "%, Aktivierung ab =", rh_activation, "%, Kritisch =", is_critical); if (ti > MAX_HEAT_CEIL) {
// Warm Zone (> 20°C): Maximize cooling. Prevent heating if To > Ti.
// Sicherheitsprüfung kommen regelmäßig frische Daten von den Sensoren? govState = 3;
lost_connection_innen = lost_connection_innen + schaltzeit if (to > ti) return { run: false, reason: "HEATING_PREVENTED", maxRun: 0, qual: 0, gov: govState };
lost_connection_aussen = lost_connection_aussen + schaltzeit reqSpread = DP_SPREAD_BASE;
print("letzte Verbindung zum Sensor innen vor " ,lost_connection_innen, " Sekunden " ); targetHumi = TARGET_SUMMER;
print("letzte Verbindung zum Sensor außen vor " ,lost_connection_aussen, " Sekunden " ); } else if (ti >= WINTER_IN_TH) {
// Comfort Zone (14°C - 20°C)
if (lost_connection_innen > lost_connection || govState = 2;
lost_connection_aussen > lost_connection ) targetHumi = TARGET_SUMMER;
{ if (to < (ti - 5.0)) {
print("Verbindung zu Sensoren zu lange verloren, Lüfter ausschalten."); reqSpread = DP_SPREAD_BASE + 1.0; // Strong cooling requires prime spread >= 3.0°C
Shelly.call("Switch.Set", { id: 0, on: false }); }
farbring(80,80,0,100);
return;
}
// Visualisierung Batteriefüllstand durch roten Blink
if (battery_innen < battery_warngrenze ||
battery_aussen < battery_warngrenze)
{
print("Batteriestand niedrig");
farbring(100,0,0,100);
}
// Schaltlogik (immer schalten, der Shelly schaltet nur, wenn er schaltet muss).
if ( is_critical &&
temperatur_innen > mindesttemperatur &&
humidity_innen > mindesthumi &&
taupunkt_innen > taupunkt_aussen + taupunktschwelle
)
{
print("Lüfter einschalten");
Shelly.call("Switch.Set", { id: 0, on: true });
farbring(80,10,0,100);
} else { } else {
print("Lüfter ausschalten."); // Cold Zone (< 14°C)
Shelly.call("Switch.Set", { id: 0, on: false }); govState = 4;
farbring(0,0,80,100); targetHumi = TARGET_WINTER;
} if (to >= ti) {
} reqSpread = DP_SPREAD_BASE + 0.5; // Warming cellar allowed at spread >= 2.5°C
} else {
// Farbe Farbring setzen für Standalone Betrieb. reqSpread = DP_SPREAD_BASE + 1.5; // Further cooling an already cold cellar requires >= 3.5°C
function farbring(red,green,blue,helligkeit) { }
Shelly.call(
"PLUGS_UI.SetConfig",{ id:0, config:{"leds":{"mode":"switch","colors":
{"switch:0":
{"on":{"rgb":[red,green,blue],"brightness":helligkeit},
"off":{"rgb":[red,green,blue],"brightness":helligkeit}}}}}},
function (result, code, msg, ud) {
},
null
);
}
// Event-Verarbeitung
function checkBlu(event) {
var addr = event.address.toLowerCase();
var aussen = sensor_aussen.toLowerCase();
var innen = sensor_innen.toLowerCase();
if (DEBUG) {
print("DEBUG checkBlu: event.addr=", addr, " aussen=", aussen, " innen=", innen);
} }
if (addr === aussen) { // 3. Target Humidity Gate
if (typeof event.temperature !== "undefined") { if (hi <= targetHumi) {
temperatur_aussen = event.temperature; return { run: false, reason: "TARGET_REACHED", maxRun: 0, qual: 0, gov: govState };
}
if (typeof event.humidity !== "undefined") {
humidity_aussen = event.humidity;
}
if (typeof temperatur_aussen !== "undefined" && typeof humidity_aussen !== "undefined") {
taupunkt_aussen = taupunkt(temperatur_aussen, humidity_aussen);
}
if (typeof event.battery !== "undefined") {
battery_aussen = event.battery;
}
lost_connection_aussen = 0;
print("Neue Werte für Außen: T=", temperatur_aussen, "°C, RH=", humidity_aussen, "%, Tp=", taupunkt_aussen, "°C, Batt=", battery_aussen, "%");
} else if (addr === innen) {
if (typeof event.temperature !== "undefined") {
temperatur_innen = event.temperature;
}
if (typeof event.humidity !== "undefined") {
humidity_innen = event.humidity;
}
if (typeof temperatur_innen !== "undefined" && typeof humidity_innen !== "undefined") {
taupunkt_innen = taupunkt(temperatur_innen, humidity_innen);
}
if (typeof event.battery !== "undefined") {
battery_innen = event.battery;
}
lost_connection_innen = 0;
print("Neue Werte für Innen: T=", temperatur_innen, "°C, RH=", humidity_innen, "%, Tp=", taupunkt_innen, "°C, Batt=", battery_innen, "%");
} }
// 4. Dew Point Spread Gate
var spread = di - do_;
if (spread < reqSpread) {
return { run: false, reason: "INSUFFICIENT_SPREAD", maxRun: 0, qual: Math.round(Math.max(0, spread * 20)), gov: govState };
}
// 5. Dynamic Quality Runtime Formula
// Runtime [min] = (spread - DP_SPREAD_BASE) * 45 min
var maxRunMin = 0;
if (spread >= 5.0) {
maxRunMin = 9999; // Unlimited continuous run
} else {
maxRunMin = Math.round((spread - DP_SPREAD_BASE) * 45);
if (maxRunMin < 15) maxRunMin = 15;
}
// Cold Zone burst throttle (max 25 min bursts when Ti < 14°C)
if (ti < WINTER_IN_TH && maxRunMin > 25) {
maxRunMin = 25;
}
var qualityScore = Math.min(100, Math.round(spread * 20));
return { run: true, reason: "VENTILATING_QUALITY", maxRun: maxRunMin, qual: qualityScore, gov: govState };
} }
// Haupt-Timer für Steuerlogik function updateLeds(c) {
Timer.set(schaltzeit * 1000, true, function () { if (c === last_color) return;
var rh_crit_t = (typeof temperatur_innen !== "undefined") ? (critical_humi_base - critical_humi_temp_coeff * (temperatur_innen - critical_humi_ref_temp)) : undefined; last_color = c;
var rh_act_t = (typeof rh_crit_t !== "undefined") ? (rh_crit_t - critical_humi_buffer) : undefined; var r = 0, g = 40, b = 80, br = 60;
if (c === 1) { r = 100; g = 0; b = 0; br = 100; }
else if (c === 2) { r = 80; g = 80; b = 0; br = 100; }
else if (c === 3) { r = 60; g = 0; b = 80; br = 80; }
else if (c === 4) { r = 80; g = 20; b = 0; br = 100; }
print("----- Steuerung alle", schaltzeit, "s -----"); Shelly.call("PLUGS_UI.SetConfig", {
print("Innen: T =", temperatur_innen, "°C, RH =", humidity_innen, "%, Tp =", taupunkt_innen, "°C, Aktivierung ab RH =", rh_act_t, "% (Batterie:", battery_innen, "%)"); id: 0,
print("Außen: T =", temperatur_aussen, "°C, RH =", humidity_aussen, "%, Tp =", taupunkt_aussen, "°C (Batterie:", battery_aussen, "%)"); config: { leds: { mode: "switch", colors: { "switch:0": { on: { rgb: [r, g, b], brightness: br }, off: { rgb: [r, g, b], brightness: br } } } } }
schalten(); });
}
function applyDecision(d) {
last_reason = d.reason;
st_quality = d.qual;
st_max_run_min = d.maxRun;
st_gov_state = d.gov;
if (d.run) {
fan_run_sec += SCHALTZEIT_SEC;
if (d.maxRun < 9999 && fan_run_sec >= d.maxRun * 60) {
d.run = false;
last_reason = "BURST_LIMIT_REACHED";
fan_pause_sec = MIN_PAUSE_MIN * 60;
fan_run_sec = 0;
}
} else {
fan_run_sec = 0;
if (fan_pause_sec > 0) {
fan_pause_sec -= SCHALTZEIT_SEC;
if (fan_pause_sec < 0) fan_pause_sec = 0;
}
}
if (d.run !== relay_on) {
print("Relais:", d.run ? "EIN" : "AUS", "|", last_reason, "| MaxRun:", st_max_run_min, "min | Qual:", st_quality);
Shelly.call("Switch.Set", { id: 0, on: d.run });
relay_on = d.run;
}
var c = 5;
if (bi < BATT_LIMIT || bo < BATT_LIMIT) c = 1;
else if (age_in > LOST_LIMIT || age_out > LOST_LIMIT) c = 2;
else if (fan_pause_sec > 0) c = 3;
else if (d.run) c = 4;
updateLeds(c);
var isWinter = (typeof ti === "number") ? (ti < WINTER_IN_TH) : false;
var mt = calcMoldThresh(ti);
var mc = (typeof ti === "number" && typeof hi === "number" && typeof mt === "number" && hi >= mt) ? 1 : 0;
st_run = d.run ? 1 : 0;
st_target = isWinter ? TARGET_WINTER : TARGET_SUMMER;
st_mold_th = (typeof mt === "number") ? mt : 70.0;
st_mold_crit = mc;
st_spread = (typeof di === "number" && typeof do_ === "number") ? (di - do_) : 0;
}
Timer.set(SCHALTZEIT_SEC * 1000, true, function () {
age_in += SCHALTZEIT_SEC;
age_out += SCHALTZEIT_SEC;
var d = evaluateDecision();
print("Taupi:", d.run, d.reason, "| Qual:", d.qual, "| Spread:", st_spread);
applyDecision(d);
}); });
///////////////// BLE-Decoder /////////////////////// function decodeBTHome(buf, isOut) {
const BTHOME_SVC_ID_STR = "fcd2"; if (typeof buf !== "string" || buf.length < 2) return;
var dib = buf.at(0);
if ((dib >> 5) !== 2 || (dib & 0x1)) return;
var off = 1, pid = -1, t = undefined, h = undefined, b = undefined;
const uint8 = 0; while (off < buf.length) {
const int8 = 1; var id = buf.at(off++);
const uint16 = 2; if (id === 0x00) { pid = buf.at(off++); }
const int16 = 3; else if (id === 0x01) { b = buf.at(off++); }
else if (id === 0x2e) { h = buf.at(off++); }
else if (id === 0x1e || id === 0x20 || id === 0x21 || id === 0x2d || id === 0x3a || id === 0x60) { off++; }
else if (id === 0x02) {
var u = (buf.at(off + 1) << 8) | buf.at(off);
off += 2;
var raw = (u & 0x8000) ? (u - 0x10000) : u;
t = raw * 0.01;
} else if (id === 0x03) {
var u = (buf.at(off + 1) << 8) | buf.at(off);
off += 2;
h = u * 0.01;
} else if (id === 0x45) {
var u = (buf.at(off + 1) << 8) | buf.at(off);
off += 2;
var raw = (u & 0x8000) ? (u - 0x10000) : u;
t = raw * 0.1;
} else if (id === 0x08 || id === 0x0c || id === 0x3c || id === 0x3f || id === 0x44 || id === 0x46 || id === 0x5e || id === 0x5f || id === 0xf0) {
off += 2;
} else if (id === 0x04 || id === 0x05 || id === 0xf2) {
off += 3;
} else if (id === 0xf1) {
off += 4;
} else {
break;
}
}
// The BTH object defines the structure of the BTHome data (trimmed to temperature/humidity/battery) if (isOut) {
const BTH = { if (pid !== -1) {
0x00: { n: "pid", t: uint8 }, if (pid === pid_out && typeof t !== "number") return;
0x01: { n: "battery", t: uint8 }, pid_out = pid;
0x02: { n: "temperature", t: int16, f: 0.01 }, }
0x03: { n: "humidity", t: uint16, f: 0.01 }, if (typeof t === "number") to = t;
0x1e: { n: "light", t: uint8 }, if (typeof h === "number") ho = h;
0x2d: { n: "window", t: uint8 }, if (typeof to === "number" && typeof ho === "number") do_ = calcDewPoint(to, ho);
0x2e: { n: "humidity", t: uint8 }, if (typeof b === "number") bo = b;
0x3f: { n: "rotation", t: int16, f: 0.1 }, age_out = 0;
0x45: { n: "temperature", t: int16, f: 0.1 } } else {
}; if (pid !== -1) {
if (pid === pid_in && typeof t !== "number") return;
function getByteSize(type) { pid_in = pid;
if (type === uint8 || type === int8) return 1; }
if (type === uint16 || type === int16) return 2; if (typeof t === "number") ti = t;
return 255; if (typeof h === "number") hi = h;
if (typeof ti === "number" && typeof hi === "number") di = calcDewPoint(ti, hi);
if (typeof b === "number") bi = b;
age_in = 0;
}
} }
// Functions for decoding and unpacking the service data from Shelly BLU devices
const BTHomeDecoder = {
utoi: function (num, bitsz) {
const mask = 1 << (bitsz - 1);
return num & mask ? num - (1 << bitsz) : num;
},
getUInt8: function (buffer, offset) {
return buffer.at(offset);
},
getInt8: function (buffer, offset) {
return this.utoi(this.getUInt8(buffer, offset), 8);
},
getUInt16LE: function (buffer, offset) {
return 0xffff & ((buffer.at(offset + 1) << 8) | buffer.at(offset));
},
getInt16LE: function (buffer, offset) {
return this.utoi(this.getUInt16LE(buffer, offset), 16);
},
getBufValue: function (type, buffer, offset) {
if (buffer.length - offset < getByteSize(type)) return null;
if (type === uint8) return this.getUInt8(buffer, offset);
if (type === int8) return this.getInt8(buffer, offset);
if (type === uint16) return this.getUInt16LE(buffer, offset);
if (type === int16) return this.getInt16LE(buffer, offset);
return null;
},
unpack: function (buffer) {
if (typeof buffer !== "string" || buffer.length === 0) return null;
let result = {};
let _dib = buffer.at(0);
result["encryption"] = _dib & 0x1 ? true : false;
result["BTHome_version"] = _dib >> 5;
if (result["BTHome_version"] !== 2) return null;
if (result["encryption"]) return result;
let offset = 1;
let _bth;
let _value;
while (offset < buffer.length) {
_bth = BTH[buffer.at(offset)];
if (typeof _bth === "undefined") {
break;
}
offset++;
_value = this.getBufValue(_bth.t, buffer, offset);
if (_value === null) break;
if (typeof _bth.f !== "undefined") _value = _value * _bth.f;
if (typeof result[_bth.n] === "undefined") {
result[_bth.n] = _value;
}
else {
if (Array.isArray(result[_bth.n])) {
result[_bth.n].push(_value);
}
else {
result[_bth.n] = [
result[_bth.n],
_value
];
}
}
offset += getByteSize(_bth.t);
}
return result;
},
};
let lastPacketId = 0x100;
function BLEScanCallback(event, result) { function BLEScanCallback(event, result) {
if (event !== BLE.Scanner.SCAN_RESULT) { if (event !== BLE.Scanner.SCAN_RESULT || !result || typeof result.addr !== "string") return;
return; var a = result.addr.toLowerCase();
} var isOut = (a === MAC_OUT), isIn = (a === MAC_IN);
if (!isOut && !isIn) return;
if (DEBUG) { if (!result.service_data || typeof result.service_data["fcd2"] === "undefined") return;
print("DEBUG BLEScanCallback: addr=", result.addr, " rssi=", result.rssi); decodeBTHome(result.service_data["fcd2"], isOut);
}
if (typeof result.service_data === "undefined" ||
typeof result.service_data[BTHOME_SVC_ID_STR] === "undefined") {
return;
}
let unpackedData = BTHomeDecoder.unpack(result.service_data[BTHOME_SVC_ID_STR]);
if (unpackedData === null ||
typeof unpackedData === "undefined" ||
unpackedData["encryption"]) {
print("Error: Encrypted devices are not supported or unpacking failed");
return;
}
if (DEBUG) {
print("DEBUG unpacked BTHome:", JSON.stringify(unpackedData));
}
if (lastPacketId === unpackedData.pid) {
return;
}
lastPacketId = unpackedData.pid;
unpackedData.address = result.addr;
checkBlu(unpackedData);
} }
function initBLE() { function initBLE() {
const BLEConfig = Shelly.getComponentConfig("ble"); var c = Shelly.getComponentConfig("ble");
if (!c || !c.enable) return;
if (!BLEConfig.enable) { if (!BLE.Scanner.isRunning()) {
print("Error: The Bluetooth is not enabled, please enable it from settings"); BLE.Scanner.Start({ duration_ms: BLE.Scanner.INFINITE_SCAN, active: false });
return;
} }
if (BLE.Scanner.isRunning()) {
print("Info: The BLE gateway is running, the BLE scan configuration is managed by the device");
}
else {
const bleScanner = BLE.Scanner.Start({
duration_ms: BLE.Scanner.INFINITE_SCAN,
active: false
});
if(!bleScanner) {
print("Error: Can not start new scanner");
}
}
BLE.Scanner.Subscribe(BLEScanCallback); BLE.Scanner.Subscribe(BLEScanCallback);
} }
initBLE(); initBLE();
// Expose metrics for Prometheus function g(n, v) {
return (typeof v === "number" && !isNaN(v)) ? (n + " " + JSON.stringify(v) + "\n") : "";
}
if (typeof HTTPServer !== "undefined" && typeof HTTPServer.registerEndpoint === "function") { if (typeof HTTPServer !== "undefined" && typeof HTTPServer.registerEndpoint === "function") {
HTTPServer.registerEndpoint("prometheus", function(req, res) { HTTPServer.registerEndpoint("prometheus", function (req, res) {
var body = "";
function addGauge(name, val, help) {
if ((typeof val === "number" && !isNaN(val)) || typeof val === "boolean") {
var numVal = (typeof val === "boolean") ? (val ? 1 : 0) : val;
body += name + " " + numVal + "\n";
}
}
// Calculate helper values
var rh_crit = (typeof temperatur_innen !== "undefined" && !isNaN(temperatur_innen)) ? (critical_humi_base - critical_humi_temp_coeff * (temperatur_innen - critical_humi_ref_temp)) : undefined;
var rh_activation = (typeof rh_crit !== "undefined" && !isNaN(rh_crit)) ? (rh_crit - critical_humi_buffer) : undefined;
var is_critical_val = (typeof humidity_innen !== "undefined" && !isNaN(humidity_innen) && typeof rh_activation !== "undefined" && !isNaN(rh_activation)) ? (humidity_innen >= rh_activation) : undefined;
var would_fan_activate = false;
if (typeof is_critical_val !== "undefined" && is_critical_val !== null &&
typeof temperatur_innen !== "undefined" && !isNaN(temperatur_innen) &&
typeof humidity_innen !== "undefined" && !isNaN(humidity_innen) &&
typeof taupunkt_innen !== "undefined" && !isNaN(taupunkt_innen) &&
typeof taupunkt_aussen !== "undefined" && !isNaN(taupunkt_aussen)) {
would_fan_activate = is_critical_val &&
(temperatur_innen > mindesttemperatur) &&
(humidity_innen > mindesthumi) &&
(taupunkt_innen > taupunkt_aussen + taupunktschwelle);
}
addGauge("taupi_temperature_celsius_innen", temperatur_innen, "Indoor temperature in Celsius");
addGauge("taupi_temperature_celsius_aussen", temperatur_aussen, "Outdoor temperature in Celsius");
addGauge("taupi_humidity_percent_innen", humidity_innen, "Indoor relative humidity in percent");
addGauge("taupi_humidity_percent_aussen", humidity_aussen, "Outdoor relative humidity in percent");
addGauge("taupi_dewpoint_celsius_innen", taupunkt_innen, "Indoor dew point in Celsius");
addGauge("taupi_dewpoint_celsius_aussen", taupunkt_aussen, "Outdoor dew point in Celsius");
addGauge("taupi_battery_percent_innen", battery_innen, "Indoor sensor battery percentage");
addGauge("taupi_battery_percent_aussen", battery_aussen, "Outdoor sensor battery percentage");
addGauge("taupi_lost_connection_seconds_innen", lost_connection_innen, "Seconds since last contact with indoor sensor");
addGauge("taupi_lost_connection_seconds_aussen", lost_connection_aussen, "Seconds since last contact with outdoor sensor");
addGauge("taupi_critical_humidity_threshold_percent", rh_activation, "Calculated relative humidity threshold above which it is critical");
addGauge("taupi_is_critical", is_critical_val, "Flag indicating mold danger inside (humidity exceeds threshold)");
addGauge("taupi_would_fan_activate", would_fan_activate, "Flag indicating if all fan activation criteria are met");
var relay_status = Shelly.getComponentStatus("switch", 0);
if (relay_status && typeof relay_status.output !== "undefined") {
addGauge("taupi_relay_status", relay_status.output, "Physical switch status (1 = on, 0 = off)");
}
res.body = body;
res.headers = [["Content-Type", "text/plain; version=0.0.4"]];
res.code = 200; res.code = 200;
res.headers = [["Content-Type", "text/plain; version=0.0.4"]];
res.body =
g("taupi_temperature_celsius_innen", ti) +
g("taupi_temperature_celsius_aussen", to) +
g("taupi_humidity_percent_innen", hi) +
g("taupi_humidity_percent_aussen", ho) +
g("taupi_dewpoint_celsius_innen", di) +
g("taupi_dewpoint_celsius_aussen", do_) +
g("taupi_dewpoint_spread_celsius", st_spread) +
g("taupi_target_humidity_percent", st_target) +
g("taupi_critical_humidity_threshold_percent", st_mold_th) +
g("taupi_is_critical", st_mold_crit) +
g("taupi_would_fan_activate", st_run) +
g("taupi_quality_score", st_quality) +
g("taupi_max_allowed_runtime_minutes", st_max_run_min) +
g("taupi_thermal_governor_state", st_gov_state) +
g("taupi_fan_running_seconds", fan_run_sec) +
g("taupi_fan_pause_remaining_seconds", fan_pause_sec) +
g("taupi_battery_percent_innen", bi) +
g("taupi_battery_percent_aussen", bo) +
g("taupi_lost_connection_seconds_innen", age_in) +
g("taupi_lost_connection_seconds_aussen", age_out) +
g("taupi_relay_status", relay_on ? 1 : 0);
res.send(); res.send();
}); });
} }
+62 -7
View File
@@ -17,19 +17,73 @@ variable "sensor_innen_mac" {
variable "taupunktschwelle" { variable "taupunktschwelle" {
type = number type = number
description = "Dew point difference threshold [°C] to trigger the fan (indoor_dp > outdoor_dp + threshold)" description = "Dew point difference threshold [°C] to trigger the fan (indoor_dp > outdoor_dp + threshold)"
default = 2 default = 2.0
} }
variable "mindesttemperatur" { variable "mindesttemperatur" {
type = number type = number
description = "Minimum indoor temperature [°C] required to run the fan" description = "Minimum indoor temperature [°C] required to run the fan (frost / cold protection)"
default = 10 default = 10.0
} }
variable "mindesthumi" { variable "max_indoor_temp_heating" {
type = number type = number
description = "Minimum indoor relative humidity [%] required to run the fan" description = "Maximum cellar temperature [°C] below which heating with outdoor air is permitted. Above this, T_out must be <= T_in."
default = 50 default = 20.0
}
variable "winter_temp_threshold_in" {
type = number
description = "Indoor temperature threshold [°C] below which winter mode is active"
default = 14.0
}
variable "winter_temp_threshold_out" {
type = number
description = "Outdoor temperature threshold [°C] below which winter mode is active"
default = 10.0
}
variable "target_humi_summer" {
type = number
description = "Target indoor relative humidity [%] during warm / summer mode"
default = 40.0
}
variable "target_humi_winter" {
type = number
description = "Target indoor relative humidity [%] during cold / winter mode (avoids over-cooling)"
default = 50.0
}
variable "urgent_humi_threshold" {
type = number
description = "Relative humidity threshold [%] above which ventilation is urgently evaluated 24/7"
default = 60.0
}
variable "night_window_start_hour" {
type = number
description = "Start hour (0-23) for the optimal dew point drying window"
default = 3
}
variable "night_window_end_hour" {
type = number
description = "End hour (0-23) for the optimal dew point drying window"
default = 7
}
variable "max_runtime_minutes" {
type = number
description = "Maximum continuous fan runtime in minutes before enforcing a cooldown pause"
default = 90
}
variable "min_pause_minutes" {
type = number
description = "Minimum rest period in minutes after a ventilation burst to allow thermal/humidity equilibration"
default = 20
} }
variable "schaltzeit" { variable "schaltzeit" {
@@ -70,6 +124,7 @@ variable "critical_humi_ref_temp" {
variable "critical_humi_buffer" { variable "critical_humi_buffer" {
type = number type = number
description = "Safety buffer [%] subtracted from the critical relative humidity threshold" description = "Safety buffer [%] subtracted from the critical relative humidity threshold for mold alert"
default = 10 default = 10
} }
+17
View File
@@ -0,0 +1,17 @@
FROM python:3.11-slim
WORKDIR /app
# Prevent Python from writing .pyc files and buffer stdout/stderr
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
+18
View File
@@ -0,0 +1,18 @@
REGISTRY ?= registry.haumdaucher.de
IMAGE_NAME ?= website/grabengasse_backend
TAG ?= latest
IMAGE ?= $(REGISTRY)/$(IMAGE_NAME):$(TAG)
.PHONY: build tag push all
all: build tag push
build:
docker build -t $(IMAGE_NAME):$(TAG) .
tag:
docker tag $(IMAGE_NAME):$(TAG) $(IMAGE)
push:
docker push $(IMAGE)
+22
View File
@@ -0,0 +1,22 @@
import os
class Settings:
PROJECT_NAME: str = "Grabengasse 7 Backend API"
VERSION: str = "1.0.0"
API_V1_STR: str = "/api/v1"
# Prometheus K8s cluster service URL (with fallback to localhost/dev)
PROMETHEUS_URL: str = os.getenv(
"PROMETHEUS_URL",
"http://prometheus-operator-kube-p-prometheus.monitoring.svc.cluster.local:9090"
)
CORS_ORIGINS: list[str] = [
"http://localhost:5173",
"http://localhost:80",
"https://grabengasse7.de",
"http://grabengasse7.de",
"*"
]
settings = Settings()
+37
View File
@@ -0,0 +1,37 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from config import settings
from routers import weather
app = FastAPI(
title=settings.PROJECT_NAME,
version=settings.VERSION,
openapi_url=f"{settings.API_V1_STR}/openapi.json",
docs_url=f"{settings.API_V1_STR}/docs",
redoc_url=f"{settings.API_V1_STR}/redoc",
)
# CORS middleware configuration
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include routers
app.include_router(weather.router, prefix=settings.API_V1_STR)
@app.get("/healthz", tags=["Health"])
@app.get(f"{settings.API_V1_STR}/healthz", tags=["Health"])
async def health_check():
return {
"status": "healthy",
"service": settings.PROJECT_NAME,
"version": settings.VERSION
}
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
@@ -0,0 +1,4 @@
fastapi>=0.110.0
uvicorn[standard]>=0.28.0
httpx>=0.27.0
pydantic>=2.6.0
@@ -0,0 +1,49 @@
from fastapi import APIRouter, Query, HTTPException, status
from schemas.weather import CurrentWeatherMetrics, WeatherHistoryResponse
from services.prometheus import prometheus_service
router = APIRouter(prefix="/weather", tags=["Weather"])
@router.get(
"/current",
response_model=CurrentWeatherMetrics,
summary="Get current weather metrics",
description="Returns live weather metrics from the Grabengasse 7 weather station via Prometheus."
)
async def get_current_weather():
try:
metrics = await prometheus_service.get_current_metrics()
return metrics
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch current weather metrics: {str(e)}"
)
@router.get(
"/history",
response_model=WeatherHistoryResponse,
summary="Get weather historical time series data",
description="Returns historical time series metrics (temperature, humidity, wind, UV, illuminance) for interactive charts."
)
async def get_weather_history(
hours: int = Query(24, ge=1, le=10000, description="History duration in hours (1h to 8760h / 12 months)")
):
try:
# Calculate appropriate step size based on duration
if hours <= 24:
step = 300 # 5 min intervals (~288 points)
elif hours <= 168:
step = 1800 # 30 min intervals (~336 points)
elif hours <= 720:
step = 7200 # 2 hour intervals (~360 points)
else:
step = 86400 # 24 hour intervals (~365 points for 12 months)
history = await prometheus_service.get_history(hours=hours, step=step)
return history
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch weather historical data: {str(e)}"
)
@@ -0,0 +1,35 @@
from pydantic import BaseModel, Field
from typing import Optional, List, Dict
class CurrentWeatherMetrics(BaseModel):
temperature: Optional[float] = Field(None, description="Current temperature in °C")
apparent_temperature: Optional[float] = Field(None, description="Apparent temperature in °C")
wind_chill: Optional[float] = Field(None, description="Wind chill temperature in °C")
dew_point: Optional[float] = Field(None, description="Dew point in °C")
humidex: Optional[float] = Field(None, description="Humidex in °C")
humidity: Optional[float] = Field(None, description="Relative humidity percentage (%)")
pressure: Optional[float] = Field(None, description="Atmospheric pressure in hPa")
rain_status: Optional[int] = Field(None, description="Rain status (0: Dry, 1: Wet)")
rain_status_label: Optional[str] = Field("Dry", description="Human readable rain status label")
wind_speed: Optional[float] = Field(None, description="Wind speed in km/h")
gust_speed: Optional[float] = Field(None, description="Gust speed in km/h")
wind_direction: Optional[float] = Field(None, description="Wind direction in degrees (0-360°)")
illuminance_lux: Optional[float] = Field(None, description="Solar illuminance in LUX")
uv_index: Optional[float] = Field(None, description="UV Index")
battery_percent: Optional[float] = Field(None, description="Battery level (%)")
linkquality_lqi: Optional[float] = Field(None, description="Zigbee signal strength (LQI)")
updated_at: str = Field(..., description="Timestamp of metric evaluation")
class TimeSeriesPoint(BaseModel):
timestamp: int
value: float
class TimeSeriesMetric(BaseModel):
name: str
label: str
unit: str
data: List[TimeSeriesPoint]
class WeatherHistoryResponse(BaseModel):
timeframe: str
metrics: Dict[str, TimeSeriesMetric]
@@ -0,0 +1,181 @@
import logging
import math
import time
from typing import Dict, Any, Optional, List
import httpx
from config import settings
from schemas.weather import CurrentWeatherMetrics, TimeSeriesPoint, TimeSeriesMetric, WeatherHistoryResponse
logger = logging.getLogger("grabengasse_backend.prometheus")
def calculate_dew_point(temp: Optional[float], humidity: Optional[float]) -> Optional[float]:
"""Calculate dew point (°C) using the Magnus-Tetens formula."""
if temp is None or humidity is None or humidity <= 0:
return None
a = 17.625
b = 243.04
try:
alpha = ((a * temp) / (b + temp)) + math.log(humidity / 100.0)
dew_point = (b * alpha) / (a - alpha)
return round(dew_point, 1)
except Exception:
return None
METRIC_QUERIES = {
"temperature": 'homeassistant_sensor_temperature_celsius{entity=~"sensor.shelly_ws90_temperature|sensor.shelly_ws90_temperatur"}',
"apparent_temperature": 'homeassistant_sensor_temperature_celsius{entity="sensor.shelly_ws90_apparent_temperature"}',
"wind_chill": 'homeassistant_sensor_temperature_celsius{entity="sensor.shelly_ws90_wind_chill"}',
"dew_point": 'homeassistant_sensor_temperature_celsius{entity="sensor.shelly_ws90_dew_point"}',
"humidex": 'homeassistant_sensor_temperature_celsius{entity="sensor.shelly_ws90_humidex"}',
"humidity": 'homeassistant_sensor_humidity_percent{entity=~"sensor.shelly_ws90_humidity|sensor.shelly_ws90_luftfeuchtigkeit"}',
"pressure": 'homeassistant_sensor_atmospheric_pressure_hpa{entity=~"sensor.shelly_ws90_pressure|sensor.shelly_ws90_atmospharischer_druck"}',
"rain_status": 'homeassistant_binary_sensor_state{entity=~"binary_sensor.shelly_ws90_rain_status|binary_sensor.shelly_ws90_rain"}',
"wind_direction": 'homeassistant_sensor_unit_u0xb0{entity="sensor.shelly_ws90_wind_direction"} or on() vector(0)',
"wind_speed": 'homeassistant_sensor_wind_speed_km_per_h{entity="sensor.shelly_ws90_wind_speed"} or on() vector(0)',
"gust_speed": 'homeassistant_sensor_wind_speed_km_per_h{entity="sensor.shelly_ws90_gust_speed"} or on() vector(0)',
"illuminance_lux": 'homeassistant_sensor_illuminance_lx{entity="sensor.shelly_ws90_illuminance"}',
"uv_index": 'homeassistant_sensor_state{entity="sensor.shelly_ws90_uv_index"} or on() vector(0)',
"battery_percent": 'homeassistant_sensor_battery_percent{entity=~"sensor.shelly_ws90_battery|sensor.shelly_ws90_batterie"}',
"linkquality_lqi": 'homeassistant_sensor_unit_lqi{entity=~"sensor.shelly_ws90_linkquality|sensor.shelly_ws90_lqi"}'
}
METRIC_METADATA = {
"temperature": {"label": "Temperature", "unit": "°C"},
"apparent_temperature": {"label": "Apparent Temperature", "unit": "°C"},
"dew_point": {"label": "Dew Point", "unit": "°C"},
"humidity": {"label": "Relative Humidity", "unit": "%"},
"pressure": {"label": "Atmospheric Pressure", "unit": "hPa"},
"wind_speed": {"label": "Wind Speed", "unit": "km/h"},
"gust_speed": {"label": "Gust Speed", "unit": "km/h"},
"illuminance_lux": {"label": "Solar Illuminance", "unit": "LUX"},
"uv_index": {"label": "UV Index", "unit": ""}
}
class PrometheusService:
def __init__(self, base_url: str = settings.PROMETHEUS_URL):
self.base_url = base_url.rstrip("/")
async def _query_instant(self, client: httpx.AsyncClient, query: str) -> Optional[float]:
try:
url = f"{self.base_url}/api/v1/query"
response = await client.get(url, params={"query": query}, timeout=5.0)
if response.status_code == 200:
data = response.json()
results = data.get("data", {}).get("result", [])
if results:
val = results[0].get("value", [None, None])[1]
return float(val) if val is not None else None
except Exception as e:
logger.warning(f"Error querying Prometheus instant '{query}': {e}")
return None
async def get_current_metrics(self) -> CurrentWeatherMetrics:
async with httpx.AsyncClient() as client:
values: Dict[str, Any] = {}
for key, query in METRIC_QUERIES.items():
values[key] = await self._query_instant(client, query)
rain_val = values.get("rain_status")
rain_int = int(rain_val) if rain_val is not None else 0
rain_label = "Wet (Rain)" if rain_int == 1 else "Dry"
dew_point_val = values.get("dew_point")
if dew_point_val is None:
dew_point_val = calculate_dew_point(values.get("temperature"), values.get("humidity"))
return CurrentWeatherMetrics(
temperature=values.get("temperature"),
apparent_temperature=values.get("apparent_temperature"),
wind_chill=values.get("wind_chill"),
dew_point=dew_point_val,
humidex=values.get("humidex"),
humidity=values.get("humidity"),
pressure=values.get("pressure"),
rain_status=rain_int,
rain_status_label=rain_label,
wind_speed=values.get("wind_speed"),
gust_speed=values.get("gust_speed"),
wind_direction=values.get("wind_direction"),
illuminance_lux=values.get("illuminance_lux"),
uv_index=values.get("uv_index"),
battery_percent=values.get("battery_percent"),
linkquality_lqi=values.get("linkquality_lqi"),
updated_at=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
)
async def get_history(self, hours: int = 24, step: int = 300) -> WeatherHistoryResponse:
end_time = int(time.time())
start_time = end_time - (hours * 3600)
results_map: Dict[str, TimeSeriesMetric] = {}
async with httpx.AsyncClient() as client:
for key, meta in METRIC_METADATA.items():
query = METRIC_QUERIES.get(key)
if not query:
continue
try:
url = f"{self.base_url}/api/v1/query_range"
params = {
"query": query,
"start": start_time,
"end": end_time,
"step": step
}
response = await client.get(url, params=params, timeout=20.0)
points: List[TimeSeriesPoint] = []
if response.status_code == 200:
data = response.json()
res = data.get("data", {}).get("result", [])
if res:
values_raw = res[0].get("values", [])
for t, v in values_raw:
try:
points.append(TimeSeriesPoint(timestamp=int(t), value=round(float(v), 2)))
except (ValueError, TypeError):
pass
results_map[key] = TimeSeriesMetric(
name=key,
label=meta["label"],
unit=meta["unit"],
data=points
)
except Exception as e:
logger.warning(f"Error fetching history for '{key}': {e}")
results_map[key] = TimeSeriesMetric(
name=key,
label=meta["label"],
unit=meta["unit"],
data=[]
)
# Compute dew point history if missing from Prometheus
dew_metric = results_map.get("dew_point")
temp_metric = results_map.get("temperature")
humidity_metric = results_map.get("humidity")
if (not dew_metric or len(dew_metric.data) == 0) and temp_metric and humidity_metric:
hum_by_ts = {p.timestamp: p.value for p in humidity_metric.data}
computed_dew_points: List[TimeSeriesPoint] = []
for p in temp_metric.data:
h_val = hum_by_ts.get(p.timestamp)
if h_val is not None:
d_val = calculate_dew_point(p.value, h_val)
if d_val is not None:
computed_dew_points.append(TimeSeriesPoint(timestamp=p.timestamp, value=d_val))
results_map["dew_point"] = TimeSeriesMetric(
name="dew_point",
label="Dew Point",
unit="°C",
data=computed_dew_points
)
return WeatherHistoryResponse(
timeframe=f"{hours}h",
metrics=results_map
)
prometheus_service = PrometheusService()
+20
View File
@@ -0,0 +1,20 @@
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+18
View File
@@ -0,0 +1,18 @@
REGISTRY ?= registry.haumdaucher.de
IMAGE_NAME ?= website/grabengasse_frontend
TAG ?= latest
IMAGE ?= $(REGISTRY)/$(IMAGE_NAME):$(TAG)
.PHONY: build tag push all
all: build tag push
build:
docker build -t $(IMAGE_NAME):$(TAG) .
tag:
docker tag $(IMAGE_NAME):$(TAG) $(IMAGE)
push:
docker push $(IMAGE)
Binary file not shown.

After

Width:  |  Height:  |  Size: 947 B

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1019 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

+37
View File
@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, viewport-fit=cover" />
<!-- PWA & Mobile Web App Meta Tags -->
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Grabengasse 7" />
<meta name="application-name" content="Grabengasse 7" />
<meta name="theme-color" content="#0c101a" />
<meta name="msapplication-TileColor" content="#0c101a" />
<!-- Apple Touch Icons -->
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="apple-touch-icon" sizes="192x192" href="/icon-192.png" />
<link rel="apple-touch-icon" sizes="512x512" href="/icon-512.png" />
<title>Grabengasse 7 — 16-Bit Weather Station & City Telemetry</title>
<meta name="description" content="16-bit retro weather telemetry and simulation console for Grabengasse 7 in Regensburg." />
<!-- Retro Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Silkscreen:wght@400;700&family=Space+Mono:ital,wght@0,400;0,700;1,400&family=VT323&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-CiUqNiTD.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BXmywiTz.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
+26
View File
@@ -0,0 +1,26 @@
{
"name": "Grabengasse 7 — 16-Bit Weather Station",
"short_name": "Grabengasse 7",
"description": "16-bit retro weather telemetry and simulation console for Grabengasse 7 in Regensburg.",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait-primary",
"background_color": "#0c101a",
"theme_color": "#0c101a",
"categories": ["weather", "utilities"],
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}
+85
View File
@@ -0,0 +1,85 @@
const CACHE_NAME = 'grabengasse-v1.0';
const STATIC_ASSETS = [
'/',
'/index.html',
'/manifest.webmanifest',
'/icon-192.png',
'/icon-512.png',
'/apple-touch-icon.png',
'/favicon.png'
];
// Install: Cache essential shell assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(STATIC_ASSETS);
}).then(() => self.skipWaiting())
);
});
// Activate: Clean up previous cache versions
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => {
return Promise.all(
keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))
);
}).then(() => self.clients.claim())
);
});
// Fetch: Network-first for API data, Stale-while-revalidate for static assets
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// API calls: Network first with no fallback caching of stale live metrics
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(request).catch(() => {
return new Response(JSON.stringify({ error: 'Offline / Telemetry Link Offline' }), {
status: 503,
headers: { 'Content-Type': 'application/json' }
});
})
);
return;
}
// Navigation requests: Network first, fallback to cached /index.html (SPA routing)
if (request.mode === 'navigate') {
event.respondWith(
fetch(request).catch(() => {
return caches.match('/index.html');
})
);
return;
}
// Static assets (CSS, JS, Fonts, Images): Cache-first with network fallback & cache update
event.respondWith(
caches.match(request).then((cachedResponse) => {
if (cachedResponse) {
// Fetch in background to update cache
fetch(request).then((networkResponse) => {
if (networkResponse && networkResponse.status === 200) {
caches.open(CACHE_NAME).then((cache) => cache.put(request, networkResponse));
}
}).catch(() => {});
return cachedResponse;
}
return fetch(request).then((networkResponse) => {
if (!networkResponse || networkResponse.status !== 200 || networkResponse.type !== 'basic') {
return networkResponse;
}
const responseToCache = networkResponse.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, responseToCache);
});
return networkResponse;
});
})
);
});
+36
View File
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, viewport-fit=cover" />
<!-- PWA & Mobile Web App Meta Tags -->
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Grabengasse 7" />
<meta name="application-name" content="Grabengasse 7" />
<meta name="theme-color" content="#0c101a" />
<meta name="msapplication-TileColor" content="#0c101a" />
<!-- Apple Touch Icons -->
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="apple-touch-icon" sizes="192x192" href="/icon-192.png" />
<link rel="apple-touch-icon" sizes="512x512" href="/icon-512.png" />
<title>Grabengasse 7 — 16-Bit Weather Station & City Telemetry</title>
<meta name="description" content="16-bit retro weather telemetry and simulation console for Grabengasse 7 in Regensburg." />
<!-- Retro Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Silkscreen:wght@400;700&family=Space+Mono:ital,wght@0,400;0,700;1,400&family=VT323&display=swap" rel="stylesheet">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+20
View File
@@ -0,0 +1,20 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
}
+1
View File
@@ -0,0 +1 @@
../esbuild/bin/esbuild
+1
View File
@@ -0,0 +1 @@
../he/bin/he
+1
View File
@@ -0,0 +1 @@
../nanoid/bin/nanoid.cjs
+1
View File
@@ -0,0 +1 @@
../@babel/parser/bin/babel-parser.js
+1
View File
@@ -0,0 +1 @@
../rollup/dist/bin/rollup
+1
View File
@@ -0,0 +1 @@
../typescript/bin/tsc
+1
View File
@@ -0,0 +1 @@
../typescript/bin/tsserver
+1
View File
@@ -0,0 +1 @@
../vite/bin/vite.js
+1
View File
@@ -0,0 +1 @@
../vue-tsc/bin/vue-tsc.js
+794
View File
@@ -0,0 +1,794 @@
{
"name": "grabengasse-frontend",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/@babel/helper-string-parser": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.29.8",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
"integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.29.8"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.29.8",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
"integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.62.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.5.tgz",
"integrity": "sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"dev": true,
"license": "MIT"
},
"node_modules/@vitejs/plugin-vue": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
"integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"peerDependencies": {
"vite": "^5.0.0 || ^6.0.0",
"vue": "^3.2.25"
}
},
"node_modules/@volar/language-core": {
"version": "2.4.15",
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz",
"integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/source-map": "2.4.15"
}
},
"node_modules/@volar/source-map": {
"version": "2.4.15",
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz",
"integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
"dev": true,
"license": "MIT"
},
"node_modules/@volar/typescript": {
"version": "2.4.15",
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz",
"integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/language-core": "2.4.15",
"path-browserify": "^1.0.1",
"vscode-uri": "^3.0.8"
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.41",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.41.tgz",
"integrity": "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.8",
"@vue/shared": "3.5.41",
"entities": "^7.0.1",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.41",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz",
"integrity": "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==",
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.41",
"@vue/shared": "3.5.41"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.41",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz",
"integrity": "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.8",
"@vue/compiler-core": "3.5.41",
"@vue/compiler-dom": "3.5.41",
"@vue/compiler-ssr": "3.5.41",
"@vue/shared": "3.5.41",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.21",
"postcss": "^8.5.19",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.41",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz",
"integrity": "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.41",
"@vue/shared": "3.5.41"
}
},
"node_modules/@vue/compiler-vue2": {
"version": "2.7.16",
"resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
"integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
"dev": true,
"license": "MIT",
"dependencies": {
"de-indent": "^1.0.2",
"he": "^1.2.0"
}
},
"node_modules/@vue/language-core": {
"version": "2.2.12",
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz",
"integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/language-core": "2.4.15",
"@vue/compiler-dom": "^3.5.0",
"@vue/compiler-vue2": "^2.7.16",
"@vue/shared": "^3.5.0",
"alien-signals": "^1.0.3",
"minimatch": "^9.0.3",
"muggle-string": "^0.4.1",
"path-browserify": "^1.0.1"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@vue/reactivity": {
"version": "3.5.41",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.41.tgz",
"integrity": "sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.5.41"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.5.41",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.41.tgz",
"integrity": "sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.41",
"@vue/shared": "3.5.41"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.41",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.41.tgz",
"integrity": "sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.41",
"@vue/runtime-core": "3.5.41",
"@vue/shared": "3.5.41",
"csstype": "^3.2.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.41",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.41.tgz",
"integrity": "sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==",
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.5.41",
"@vue/runtime-dom": "3.5.41",
"@vue/shared": "3.5.41"
}
},
"node_modules/@vue/shared": {
"version": "3.5.41",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.41.tgz",
"integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==",
"license": "MIT"
},
"node_modules/@yr/monotone-cubic-spline": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
"integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==",
"license": "MIT"
},
"node_modules/alien-signals": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
"integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
"dev": true,
"license": "MIT"
},
"node_modules/apexcharts": {
"version": "3.54.1",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.54.1.tgz",
"integrity": "sha512-E4et0h/J1U3r3EwS/WlqJCQIbepKbp6wGUmaAwJOMjHUP4Ci0gxanLa7FR3okx6p9coi4st6J853/Cb1NP0vpA==",
"license": "MIT",
"dependencies": {
"@yr/monotone-cubic-spline": "^1.0.3",
"svg.draggable.js": "^2.2.2",
"svg.easing.js": "^2.0.0",
"svg.filter.js": "^2.0.2",
"svg.pathmorphing.js": "^0.1.3",
"svg.resize.js": "^1.4.3",
"svg.select.js": "^3.0.1"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/chart.js": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
"node_modules/de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
"dev": true,
"license": "MIT"
},
"node_modules/entities": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true,
"license": "MIT",
"bin": {
"he": "bin/he"
}
},
"node_modules/lucide-vue-next": {
"version": "0.395.0",
"resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.395.0.tgz",
"integrity": "sha512-5EwqFxv6Uwi7JgQ48G8yrsFOPe6erPkjgUX7uKDbO1+B5j11duYxILpvD1ZmjmaleiK+5mOk45bv11KLYM0Ewg==",
"deprecated": "Package deprecated. Please use @lucide/vue instead.",
"license": "ISC",
"peerDependencies": {
"vue": ">=3.0.1"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/muggle-string": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
"dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
"dev": true,
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.5.26",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.17",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/rollup": {
"version": "4.62.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.5.tgz",
"integrity": "sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.9"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@napi-rs/lzma-linux-x64-gnu": "1.5.1",
"@rollup/rollup-android-arm-eabi": "4.62.5",
"@rollup/rollup-android-arm64": "4.62.5",
"@rollup/rollup-darwin-arm64": "4.62.5",
"@rollup/rollup-darwin-x64": "4.62.5",
"@rollup/rollup-freebsd-arm64": "4.62.5",
"@rollup/rollup-freebsd-x64": "4.62.5",
"@rollup/rollup-linux-arm-gnueabihf": "4.62.5",
"@rollup/rollup-linux-arm-musleabihf": "4.62.5",
"@rollup/rollup-linux-arm64-gnu": "4.62.5",
"@rollup/rollup-linux-arm64-musl": "4.62.5",
"@rollup/rollup-linux-loong64-gnu": "4.62.5",
"@rollup/rollup-linux-loong64-musl": "4.62.5",
"@rollup/rollup-linux-ppc64-gnu": "4.62.5",
"@rollup/rollup-linux-ppc64-musl": "4.62.5",
"@rollup/rollup-linux-riscv64-gnu": "4.62.5",
"@rollup/rollup-linux-riscv64-musl": "4.62.5",
"@rollup/rollup-linux-s390x-gnu": "4.62.5",
"@rollup/rollup-linux-x64-gnu": "4.62.5",
"@rollup/rollup-linux-x64-musl": "4.62.5",
"@rollup/rollup-openbsd-x64": "4.62.5",
"@rollup/rollup-openharmony-arm64": "4.62.5",
"@rollup/rollup-win32-arm64-msvc": "4.62.5",
"@rollup/rollup-win32-ia32-msvc": "4.62.5",
"@rollup/rollup-win32-x64-gnu": "4.62.5",
"@rollup/rollup-win32-x64-msvc": "4.62.5",
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/svg.draggable.js": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
"integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
"license": "MIT",
"dependencies": {
"svg.js": "^2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.easing.js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz",
"integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==",
"license": "MIT",
"dependencies": {
"svg.js": ">=2.3.x"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.filter.js": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz",
"integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==",
"license": "MIT",
"dependencies": {
"svg.js": "^2.2.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==",
"license": "MIT"
},
"node_modules/svg.pathmorphing.js": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz",
"integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==",
"license": "MIT",
"dependencies": {
"svg.js": "^2.4.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.resize.js": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
"integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
"license": "MIT",
"dependencies": {
"svg.js": "^2.6.5",
"svg.select.js": "^2.1.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.resize.js/node_modules/svg.select.js": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
"integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
"license": "MIT",
"dependencies": {
"svg.js": "^2.2.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.select.js": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
"integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
"license": "MIT",
"dependencies": {
"svg.js": "^2.6.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/vite": {
"version": "5.4.21",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
"rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || >=20.0.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
},
"node_modules/vscode-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
"dev": true,
"license": "MIT"
},
"node_modules/vue": {
"version": "3.5.41",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.41.tgz",
"integrity": "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.41",
"@vue/compiler-sfc": "3.5.41",
"@vue/runtime-dom": "3.5.41",
"@vue/server-renderer": "3.5.41",
"@vue/shared": "3.5.41"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/vue-chartjs": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.4.tgz",
"integrity": "sha512-x3Fqob8RQvrTdssfi9ecsCzEkFOd8JPmNwSkSQzdfKj/uBsRJs/Y88cZcZIEcPsTVfMGwMo4MOoihoDG2DoE/g==",
"license": "MIT",
"peerDependencies": {
"chart.js": "^4.1.1",
"vue": "^3.0.0-0 || ^2.7.0"
}
},
"node_modules/vue-tsc": {
"version": "2.2.12",
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz",
"integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/typescript": "2.4.15",
"@vue/language-core": "2.2.12"
},
"bin": {
"vue-tsc": "bin/vue-tsc.js"
},
"peerDependencies": {
"typescript": ">=5.0.0"
}
}
}
}
@@ -0,0 +1,118 @@
// @ts-nocheck
export {};
; declare global {
const __VLS_intrinsicElements: __VLS_IntrinsicElements;
const __VLS_directiveBindingRestFields: { instance: null, oldValue: null, modifiers: any, dir: any };
const __VLS_unref: typeof import('vue').unref;
const __VLS_placeholder: any;
type __VLS_NativeElements = __VLS_SpreadMerge<SVGElementTagNameMap, HTMLElementTagNameMap>;
type __VLS_IntrinsicElements = import('vue/jsx-runtime').JSX.IntrinsicElements;
type __VLS_Element = import('vue/jsx-runtime').JSX.Element;
type __VLS_GlobalComponents = import('vue').GlobalComponents;
type __VLS_GlobalDirectives = import('vue').GlobalDirectives;
type __VLS_IsAny<T> = 0 extends 1 & T ? true : false;
type __VLS_PickNotAny<A, B> = __VLS_IsAny<A> extends true ? B : A;
type __VLS_SpreadMerge<A, B> = Omit<A, keyof B> & B;
type __VLS_WithComponent<N0 extends string, LocalComponents, Self, N1 extends string, N2 extends string, N3 extends string> =
N1 extends keyof LocalComponents ? N1 extends N0 ? Pick<LocalComponents, N0 extends keyof LocalComponents ? N0 : never> : { [K in N0]: LocalComponents[N1] } :
N2 extends keyof LocalComponents ? N2 extends N0 ? Pick<LocalComponents, N0 extends keyof LocalComponents ? N0 : never> : { [K in N0]: LocalComponents[N2] } :
N3 extends keyof LocalComponents ? N3 extends N0 ? Pick<LocalComponents, N0 extends keyof LocalComponents ? N0 : never> : { [K in N0]: LocalComponents[N3] } :
Self extends object ? { [K in N0]: Self } :
N1 extends keyof __VLS_GlobalComponents ? N1 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N1] } :
N2 extends keyof __VLS_GlobalComponents ? N2 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N2] } :
N3 extends keyof __VLS_GlobalComponents ? N3 extends N0 ? Pick<__VLS_GlobalComponents, N0 extends keyof __VLS_GlobalComponents ? N0 : never> : { [K in N0]: __VLS_GlobalComponents[N3] } :
{ [K in N0]: unknown };
type __VLS_FunctionalComponentProps<T, K> =
'__ctx' extends keyof __VLS_PickNotAny<K, {}> ? K extends { __ctx?: { props?: infer P } } ? NonNullable<P> : never
: T extends (props: infer P, ...args: any) => any ? P :
{};
type __VLS_IsFunction<T, K> = K extends keyof T
? __VLS_IsAny<T[K]> extends false
? unknown extends T[K]
? false
: true
: false
: false;
type __VLS_NormalizeComponentEvent<Props, Events, onEvent extends keyof Props, Event extends keyof Events, CamelizedEvent extends keyof Events> = (
__VLS_IsFunction<Props, onEvent> extends true
? Props
: __VLS_IsFunction<Events, Event> extends true
? { [K in onEvent]?: Events[Event] }
: __VLS_IsFunction<Events, CamelizedEvent> extends true
? { [K in onEvent]?: Events[CamelizedEvent] }
: Props
) & Record<string, unknown>;
// fix https://github.com/vuejs/language-tools/issues/926
type __VLS_UnionToIntersection<U> = (U extends unknown ? (arg: U) => unknown : never) extends ((arg: infer P) => unknown) ? P : never;
type __VLS_OverloadUnionInner<T, U = unknown> = U & T extends (...args: infer A) => infer R
? U extends T
? never
: __VLS_OverloadUnionInner<T, Pick<T, keyof T> & U & ((...args: A) => R)> | ((...args: A) => R)
: never;
type __VLS_OverloadUnion<T> = Exclude<
__VLS_OverloadUnionInner<(() => never) & T>,
T extends () => never ? never : () => never
>;
type __VLS_ConstructorOverloads<T> = __VLS_OverloadUnion<T> extends infer F
? F extends (event: infer E, ...args: infer A) => any
? { [K in E & string]: (...args: A) => void; }
: never
: never;
type __VLS_NormalizeEmits<T> = __VLS_PrettifyGlobal<
__VLS_UnionToIntersection<
__VLS_ConstructorOverloads<T> & {
[K in keyof T]: T[K] extends any[] ? { (...args: T[K]): void } : never
}
>
>;
type __VLS_PrettifyGlobal<T> = { [K in keyof T]: T[K]; } & {};
type __VLS_PickFunctionalComponentCtx<T, K> = NonNullable<__VLS_PickNotAny<
'__ctx' extends keyof __VLS_PickNotAny<K, {}> ? K extends { __ctx?: infer Ctx } ? Ctx : never : any
, T extends (props: any, ctx: infer Ctx) => any ? Ctx : any
>>;
type __VLS_UseTemplateRef<T> = Readonly<import('vue').ShallowRef<T | null>>;
function __VLS_getVForSourceType<T extends number | string | any[] | Iterable<any>>(source: T): [
item: T extends number ? number
: T extends string ? string
: T extends any[] ? T[number]
: T extends Iterable<infer T1> ? T1
: any,
index: number,
][];
function __VLS_getVForSourceType<T>(source: T): [
item: T[keyof T],
key: keyof T,
index: number,
][];
// @ts-ignore
function __VLS_getSlotParams<T>(slot: T): Parameters<__VLS_PickNotAny<NonNullable<T>, (...args: any[]) => any>>;
// @ts-ignore
function __VLS_getSlotParam<T>(slot: T): Parameters<__VLS_PickNotAny<NonNullable<T>, (...args: any[]) => any>>[0];
function __VLS_asFunctionalDirective<T>(dir: T): T extends import('vue').ObjectDirective
? NonNullable<T['created' | 'beforeMount' | 'mounted' | 'beforeUpdate' | 'updated' | 'beforeUnmount' | 'unmounted']>
: T extends (...args: any) => any
? T
: (arg1: unknown, arg2: unknown, arg3: unknown, arg4: unknown) => void;
function __VLS_makeOptional<T>(t: T): { [K in keyof T]?: T[K] };
function __VLS_asFunctionalComponent<T, K = T extends new (...args: any) => any ? InstanceType<T> : unknown>(t: T, instance?: K):
T extends new (...args: any) => any
? (props: (K extends { $props: infer Props } ? Props : any) & Record<string, unknown>, ctx?: any) => __VLS_Element & {
__ctx?: {
attrs?: any;
slots?: K extends { $slots: infer Slots } ? Slots : any;
emit?: K extends { $emit: infer Emit } ? Emit : any;
expose?(exposed: K): void;
props?: (K extends { $props: infer Props } ? Props : any) & Record<string, unknown>;
}
}
: T extends () => any ? (props: {}, ctx?: any) => ReturnType<T>
: T extends (...args: any) => any ? T
: (_: {} & Record<string, unknown>, ctx?: any) => { __ctx?: { attrs?: any, expose?: any, slots?: any, emit?: any, props?: {} & Record<string, unknown> } };
function __VLS_functionalComponentArgsRest<T extends (...args: any) => any>(t: T): 2 extends Parameters<T>['length'] ? [any] : [];
function __VLS_asFunctionalElement<T>(tag: T, endTag?: T): (attrs: T & Record<string, unknown>) => void;
function __VLS_asFunctionalSlot<S>(slot: S): S extends () => infer R ? (props: {}) => R : NonNullable<S>;
function __VLS_tryAsConstant<const T>(t: T): T;
}
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,19 @@
# @babel/helper-string-parser
> A utility package to parse strings
See our website [@babel/helper-string-parser](https://babeljs.io/docs/babel-helper-string-parser) for more information.
## Install
Using npm:
```sh
npm install --save @babel/helper-string-parser
```
or using yarn:
```sh
yarn add @babel/helper-string-parser
```
@@ -0,0 +1,295 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.readCodePoint = readCodePoint;
exports.readInt = readInt;
exports.readStringContents = readStringContents;
var _isDigit = function isDigit(code) {
return code >= 48 && code <= 57;
};
const forbiddenNumericSeparatorSiblings = {
decBinOct: new Set([46, 66, 69, 79, 95, 98, 101, 111]),
hex: new Set([46, 88, 95, 120])
};
const isAllowedNumericSeparatorSibling = {
bin: ch => ch === 48 || ch === 49,
oct: ch => ch >= 48 && ch <= 55,
dec: ch => ch >= 48 && ch <= 57,
hex: ch => ch >= 48 && ch <= 57 || ch >= 65 && ch <= 70 || ch >= 97 && ch <= 102
};
function readStringContents(type, input, pos, lineStart, curLine, errors) {
const initialPos = pos;
const initialLineStart = lineStart;
const initialCurLine = curLine;
let out = "";
let firstInvalidLoc = null;
let chunkStart = pos;
const {
length
} = input;
for (;;) {
if (pos >= length) {
errors.unterminated(initialPos, initialLineStart, initialCurLine);
out += input.slice(chunkStart, pos);
break;
}
const ch = input.charCodeAt(pos);
if (isStringEnd(type, ch, input, pos)) {
out += input.slice(chunkStart, pos);
break;
}
if (ch === 92) {
out += input.slice(chunkStart, pos);
const res = readEscapedChar(input, pos, lineStart, curLine, type === "template", errors);
if (res.ch === null && !firstInvalidLoc) {
firstInvalidLoc = {
pos,
lineStart,
curLine
};
} else {
out += res.ch;
}
({
pos,
lineStart,
curLine
} = res);
chunkStart = pos;
} else if (ch === 8232 || ch === 8233) {
++pos;
++curLine;
lineStart = pos;
} else if (ch === 10 || ch === 13) {
if (type === "template") {
out += input.slice(chunkStart, pos) + "\n";
++pos;
if (ch === 13 && input.charCodeAt(pos) === 10) {
++pos;
}
++curLine;
chunkStart = lineStart = pos;
} else {
errors.unterminated(initialPos, initialLineStart, initialCurLine);
}
} else {
++pos;
}
}
return {
pos,
str: out,
firstInvalidLoc,
lineStart,
curLine,
containsInvalid: !!firstInvalidLoc
};
}
function isStringEnd(type, ch, input, pos) {
if (type === "template") {
return ch === 96 || ch === 36 && input.charCodeAt(pos + 1) === 123;
}
return ch === (type === "double" ? 34 : 39);
}
function readEscapedChar(input, pos, lineStart, curLine, inTemplate, errors) {
const throwOnInvalid = !inTemplate;
pos++;
const res = ch => ({
pos,
ch,
lineStart,
curLine
});
const ch = input.charCodeAt(pos++);
switch (ch) {
case 110:
return res("\n");
case 114:
return res("\r");
case 120:
{
let code;
({
code,
pos
} = readHexChar(input, pos, lineStart, curLine, 2, false, throwOnInvalid, errors));
return res(code === null ? null : String.fromCharCode(code));
}
case 117:
{
let code;
({
code,
pos
} = readCodePoint(input, pos, lineStart, curLine, throwOnInvalid, errors));
return res(code === null ? null : String.fromCodePoint(code));
}
case 116:
return res("\t");
case 98:
return res("\b");
case 118:
return res("\u000b");
case 102:
return res("\f");
case 13:
if (input.charCodeAt(pos) === 10) {
++pos;
}
case 10:
lineStart = pos;
++curLine;
case 8232:
case 8233:
return res("");
case 56:
case 57:
if (inTemplate) {
return res(null);
} else {
errors.strictNumericEscape(pos - 1, lineStart, curLine);
}
default:
if (ch >= 48 && ch <= 55) {
const startPos = pos - 1;
const match = /^[0-7]+/.exec(input.slice(startPos, pos + 2));
let octalStr = match[0];
let octal = parseInt(octalStr, 8);
if (octal > 255) {
octalStr = octalStr.slice(0, -1);
octal = parseInt(octalStr, 8);
}
pos += octalStr.length - 1;
const next = input.charCodeAt(pos);
if (octalStr !== "0" || next === 56 || next === 57) {
if (inTemplate) {
return res(null);
} else {
errors.strictNumericEscape(startPos, lineStart, curLine);
}
}
return res(String.fromCharCode(octal));
}
return res(String.fromCharCode(ch));
}
}
function readHexChar(input, pos, lineStart, curLine, len, forceLen, throwOnInvalid, errors) {
const initialPos = pos;
let n;
({
n,
pos
} = readInt(input, pos, lineStart, curLine, 16, len, forceLen, false, errors, !throwOnInvalid));
if (n === null) {
if (throwOnInvalid) {
errors.invalidEscapeSequence(initialPos, lineStart, curLine);
} else {
pos = initialPos - 1;
}
}
return {
code: n,
pos
};
}
function readInt(input, pos, lineStart, curLine, radix, len, forceLen, allowNumSeparator, errors, bailOnError) {
const start = pos;
const forbiddenSiblings = radix === 16 ? forbiddenNumericSeparatorSiblings.hex : forbiddenNumericSeparatorSiblings.decBinOct;
const isAllowedSibling = radix === 16 ? isAllowedNumericSeparatorSibling.hex : radix === 10 ? isAllowedNumericSeparatorSibling.dec : radix === 8 ? isAllowedNumericSeparatorSibling.oct : isAllowedNumericSeparatorSibling.bin;
let invalid = false;
let total = 0;
for (let i = 0, e = len == null ? Infinity : len; i < e; ++i) {
const code = input.charCodeAt(pos);
let val;
if (code === 95 && allowNumSeparator !== "bail") {
const prev = input.charCodeAt(pos - 1);
const next = input.charCodeAt(pos + 1);
if (!allowNumSeparator) {
if (bailOnError) return {
n: null,
pos
};
errors.numericSeparatorInEscapeSequence(pos, lineStart, curLine);
} else if (Number.isNaN(next) || !isAllowedSibling(next) || forbiddenSiblings.has(prev) || forbiddenSiblings.has(next)) {
if (bailOnError) return {
n: null,
pos
};
errors.unexpectedNumericSeparator(pos, lineStart, curLine);
}
++pos;
continue;
}
if (code >= 97) {
val = code - 97 + 10;
} else if (code >= 65) {
val = code - 65 + 10;
} else if (_isDigit(code)) {
val = code - 48;
} else {
val = Infinity;
}
if (val >= radix) {
if (val <= 9 && bailOnError) {
return {
n: null,
pos
};
} else if (val <= 9 && errors.invalidDigit(pos, lineStart, curLine, radix)) {
val = 0;
} else if (forceLen) {
val = 0;
invalid = true;
} else {
break;
}
}
++pos;
total = total * radix + val;
}
if (pos === start || len != null && pos - start !== len || invalid) {
return {
n: null,
pos
};
}
return {
n: total,
pos
};
}
function readCodePoint(input, pos, lineStart, curLine, throwOnInvalid, errors) {
const ch = input.charCodeAt(pos);
let code;
if (ch === 123) {
++pos;
({
code,
pos
} = readHexChar(input, pos, lineStart, curLine, input.indexOf("}", pos) - pos, true, throwOnInvalid, errors));
++pos;
if (code !== null && code > 0x10ffff) {
if (throwOnInvalid) {
errors.invalidCodePoint(pos, lineStart, curLine);
} else {
return {
code: null,
pos
};
}
}
} else {
({
code,
pos
} = readHexChar(input, pos, lineStart, curLine, 4, false, throwOnInvalid, errors));
}
return {
code,
pos
};
}
//# sourceMappingURL=index.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,31 @@
{
"name": "@babel/helper-string-parser",
"version": "7.29.7",
"description": "A utility package to parse strings",
"repository": {
"type": "git",
"url": "https://github.com/babel/babel.git",
"directory": "packages/babel-helper-string-parser"
},
"homepage": "https://babel.dev/docs/en/next/babel-helper-string-parser",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"main": "./lib/index.js",
"devDependencies": {
"charcodes": "^0.2.0"
},
"engines": {
"node": ">=6.9.0"
},
"author": "The Babel Team (https://babel.dev/team)",
"exports": {
".": {
"types": "./lib/index.d.ts",
"default": "./lib/index.js"
},
"./package.json": "./package.json"
},
"type": "commonjs"
}
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,19 @@
# @babel/helper-validator-identifier
> Validate identifier/keywords name
See our website [@babel/helper-validator-identifier](https://babeljs.io/docs/babel-helper-validator-identifier) for more information.
## Install
Using npm:
```sh
npm install --save @babel/helper-validator-identifier
```
or using yarn:
```sh
yarn add @babel/helper-validator-identifier
```
@@ -0,0 +1,70 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.isIdentifierChar = isIdentifierChar;
exports.isIdentifierName = isIdentifierName;
exports.isIdentifierStart = isIdentifierStart;
let nonASCIIidentifierStartChars = "\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u037f\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u052f\u0531-\u0556\u0559\u0560-\u0588\u05d0-\u05ea\u05ef-\u05f2\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u0860-\u086a\u0870-\u0887\u0889-\u088f\u08a0-\u08c9\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u09fc\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0af9\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c39\u0c3d\u0c58-\u0c5a\u0c5c\u0c5d\u0c60\u0c61\u0c80\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cdc-\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d04-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d54-\u0d56\u0d5f-\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e81\u0e82\u0e84\u0e86-\u0e8a\u0e8c-\u0ea3\u0ea5\u0ea7-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f5\u13f8-\u13fd\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f8\u1700-\u1711\u171f-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1878\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191e\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4c\u1b83-\u1ba0\u1bae\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1c80-\u1c8a\u1c90-\u1cba\u1cbd-\u1cbf\u1ce9-\u1cec\u1cee-\u1cf3\u1cf5\u1cf6\u1cfa\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2118-\u211d\u2124\u2126\u2128\u212a-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309b-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312f\u3131-\u318e\u31a0-\u31bf\u31f0-\u31ff\u3400-\u4dbf\u4e00-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua69d\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua7dc\ua7f1-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua8fd\ua8fe\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\ua9e0-\ua9e4\ua9e6-\ua9ef\ua9fa-\ua9fe\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa7e-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uab30-\uab5a\uab5c-\uab69\uab70-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc";
let nonASCIIidentifierChars = "\xb7\u0300-\u036f\u0387\u0483-\u0487\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u0669\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7\u06e8\u06ea-\u06ed\u06f0-\u06f9\u0711\u0730-\u074a\u07a6-\u07b0\u07c0-\u07c9\u07eb-\u07f3\u07fd\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u0897-\u089f\u08ca-\u08e1\u08e3-\u0903\u093a-\u093c\u093e-\u094f\u0951-\u0957\u0962\u0963\u0966-\u096f\u0981-\u0983\u09bc\u09be-\u09c4\u09c7\u09c8\u09cb-\u09cd\u09d7\u09e2\u09e3\u09e6-\u09ef\u09fe\u0a01-\u0a03\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a66-\u0a71\u0a75\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ae2\u0ae3\u0ae6-\u0aef\u0afa-\u0aff\u0b01-\u0b03\u0b3c\u0b3e-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b55-\u0b57\u0b62\u0b63\u0b66-\u0b6f\u0b82\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0be6-\u0bef\u0c00-\u0c04\u0c3c\u0c3e-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0c66-\u0c6f\u0c81-\u0c83\u0cbc\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0ce6-\u0cef\u0cf3\u0d00-\u0d03\u0d3b\u0d3c\u0d3e-\u0d44\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0d62\u0d63\u0d66-\u0d6f\u0d81-\u0d83\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0de6-\u0def\u0df2\u0df3\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0e50-\u0e59\u0eb1\u0eb4-\u0ebc\u0ec8-\u0ece\u0ed0-\u0ed9\u0f18\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f3e\u0f3f\u0f71-\u0f84\u0f86\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u102b-\u103e\u1040-\u1049\u1056-\u1059\u105e-\u1060\u1062-\u1064\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f-\u109d\u135d-\u135f\u1369-\u1371\u1712-\u1715\u1732-\u1734\u1752\u1753\u1772\u1773\u17b4-\u17d3\u17dd\u17e0-\u17e9\u180b-\u180d\u180f-\u1819\u18a9\u1920-\u192b\u1930-\u193b\u1946-\u194f\u19d0-\u19da\u1a17-\u1a1b\u1a55-\u1a5e\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1ab0-\u1abd\u1abf-\u1add\u1ae0-\u1aeb\u1b00-\u1b04\u1b34-\u1b44\u1b50-\u1b59\u1b6b-\u1b73\u1b80-\u1b82\u1ba1-\u1bad\u1bb0-\u1bb9\u1be6-\u1bf3\u1c24-\u1c37\u1c40-\u1c49\u1c50-\u1c59\u1cd0-\u1cd2\u1cd4-\u1ce8\u1ced\u1cf4\u1cf7-\u1cf9\u1dc0-\u1dff\u200c\u200d\u203f\u2040\u2054\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2cef-\u2cf1\u2d7f\u2de0-\u2dff\u302a-\u302f\u3099\u309a\u30fb\ua620-\ua629\ua66f\ua674-\ua67d\ua69e\ua69f\ua6f0\ua6f1\ua802\ua806\ua80b\ua823-\ua827\ua82c\ua880\ua881\ua8b4-\ua8c5\ua8d0-\ua8d9\ua8e0-\ua8f1\ua8ff-\ua909\ua926-\ua92d\ua947-\ua953\ua980-\ua983\ua9b3-\ua9c0\ua9d0-\ua9d9\ua9e5\ua9f0-\ua9f9\uaa29-\uaa36\uaa43\uaa4c\uaa4d\uaa50-\uaa59\uaa7b-\uaa7d\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uaaeb-\uaaef\uaaf5\uaaf6\uabe3-\uabea\uabec\uabed\uabf0-\uabf9\ufb1e\ufe00-\ufe0f\ufe20-\ufe2f\ufe33\ufe34\ufe4d-\ufe4f\uff10-\uff19\uff3f\uff65";
const nonASCIIidentifierStart = new RegExp("[" + nonASCIIidentifierStartChars + "]");
const nonASCIIidentifier = new RegExp("[" + nonASCIIidentifierStartChars + nonASCIIidentifierChars + "]");
nonASCIIidentifierStartChars = nonASCIIidentifierChars = null;
const astralIdentifierStartCodes = [0, 11, 2, 25, 2, 18, 2, 1, 2, 14, 3, 13, 35, 122, 70, 52, 268, 28, 4, 48, 48, 31, 14, 29, 6, 37, 11, 29, 3, 35, 5, 7, 2, 4, 43, 157, 19, 35, 5, 35, 5, 39, 9, 51, 13, 10, 2, 14, 2, 6, 2, 1, 2, 10, 2, 14, 2, 6, 2, 1, 4, 51, 13, 310, 10, 21, 11, 7, 25, 5, 2, 41, 2, 8, 70, 5, 3, 0, 2, 43, 2, 1, 4, 0, 3, 22, 11, 22, 10, 30, 66, 18, 2, 1, 11, 21, 11, 25, 7, 25, 39, 55, 7, 1, 65, 0, 16, 3, 2, 2, 2, 28, 43, 28, 4, 28, 36, 7, 2, 27, 28, 53, 11, 21, 11, 18, 14, 17, 111, 72, 56, 50, 14, 50, 14, 35, 39, 27, 10, 22, 251, 41, 7, 1, 17, 5, 57, 28, 11, 0, 9, 21, 43, 17, 47, 20, 28, 22, 13, 52, 58, 1, 3, 0, 14, 44, 33, 24, 27, 35, 30, 0, 3, 0, 9, 34, 4, 0, 13, 47, 15, 3, 22, 0, 2, 0, 36, 17, 2, 24, 20, 1, 64, 6, 2, 0, 2, 3, 2, 14, 2, 9, 8, 46, 39, 7, 3, 1, 3, 21, 2, 6, 2, 1, 2, 4, 4, 0, 19, 0, 13, 4, 31, 9, 2, 0, 3, 0, 2, 37, 2, 0, 26, 0, 2, 0, 45, 52, 19, 3, 21, 2, 31, 47, 21, 1, 2, 0, 185, 46, 42, 3, 37, 47, 21, 0, 60, 42, 14, 0, 72, 26, 38, 6, 186, 43, 117, 63, 32, 7, 3, 0, 3, 7, 2, 1, 2, 23, 16, 0, 2, 0, 95, 7, 3, 38, 17, 0, 2, 0, 29, 0, 11, 39, 8, 0, 22, 0, 12, 45, 20, 0, 19, 72, 200, 32, 32, 8, 2, 36, 18, 0, 50, 29, 113, 6, 2, 1, 2, 37, 22, 0, 26, 5, 2, 1, 2, 31, 15, 0, 24, 43, 261, 18, 16, 0, 2, 12, 2, 33, 125, 0, 80, 921, 103, 110, 18, 195, 2637, 96, 16, 1071, 18, 5, 26, 3994, 6, 582, 6842, 29, 1763, 568, 8, 30, 18, 78, 18, 29, 19, 47, 17, 3, 32, 20, 6, 18, 433, 44, 212, 63, 33, 24, 3, 24, 45, 74, 6, 0, 67, 12, 65, 1, 2, 0, 15, 4, 10, 7381, 42, 31, 98, 114, 8702, 3, 2, 6, 2, 1, 2, 290, 16, 0, 30, 2, 3, 0, 15, 3, 9, 395, 2309, 106, 6, 12, 4, 8, 8, 9, 5991, 84, 2, 70, 2, 1, 3, 0, 3, 1, 3, 3, 2, 11, 2, 0, 2, 6, 2, 64, 2, 3, 3, 7, 2, 6, 2, 27, 2, 3, 2, 4, 2, 0, 4, 6, 2, 339, 3, 24, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 7, 1845, 30, 7, 5, 262, 61, 147, 44, 11, 6, 17, 0, 322, 29, 19, 43, 485, 27, 229, 29, 3, 0, 208, 30, 2, 2, 2, 1, 2, 6, 3, 4, 10, 1, 225, 6, 2, 3, 2, 1, 2, 14, 2, 196, 60, 67, 8, 0, 1205, 3, 2, 26, 2, 1, 2, 0, 3, 0, 2, 9, 2, 3, 2, 0, 2, 0, 7, 0, 5, 0, 2, 0, 2, 0, 2, 2, 2, 1, 2, 0, 3, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 1, 2, 0, 3, 3, 2, 6, 2, 3, 2, 3, 2, 0, 2, 9, 2, 16, 6, 2, 2, 4, 2, 16, 4421, 42719, 33, 4381, 3, 5773, 3, 7472, 16, 621, 2467, 541, 1507, 4938, 6, 8489];
const astralIdentifierCodes = [509, 0, 227, 0, 150, 4, 294, 9, 1368, 2, 2, 1, 6, 3, 41, 2, 5, 0, 166, 1, 574, 3, 9, 9, 7, 9, 32, 4, 318, 1, 78, 5, 71, 10, 50, 3, 123, 2, 54, 14, 32, 10, 3, 1, 11, 3, 46, 10, 8, 0, 46, 9, 7, 2, 37, 13, 2, 9, 6, 1, 45, 0, 13, 2, 49, 13, 9, 3, 2, 11, 83, 11, 7, 0, 3, 0, 158, 11, 6, 9, 7, 3, 56, 1, 2, 6, 3, 1, 3, 2, 10, 0, 11, 1, 3, 6, 4, 4, 68, 8, 2, 0, 3, 0, 2, 3, 2, 4, 2, 0, 15, 1, 83, 17, 10, 9, 5, 0, 82, 19, 13, 9, 214, 6, 3, 8, 28, 1, 83, 16, 16, 9, 82, 12, 9, 9, 7, 19, 58, 14, 5, 9, 243, 14, 166, 9, 71, 5, 2, 1, 3, 3, 2, 0, 2, 1, 13, 9, 120, 6, 3, 6, 4, 0, 29, 9, 41, 6, 2, 3, 9, 0, 10, 10, 47, 15, 199, 7, 137, 9, 54, 7, 2, 7, 17, 9, 57, 21, 2, 13, 123, 5, 4, 0, 2, 1, 2, 6, 2, 0, 9, 9, 49, 4, 2, 1, 2, 4, 9, 9, 55, 9, 266, 3, 10, 1, 2, 0, 49, 6, 4, 4, 14, 10, 5350, 0, 7, 14, 11465, 27, 2343, 9, 87, 9, 39, 4, 60, 6, 26, 9, 535, 9, 470, 0, 2, 54, 8, 3, 82, 0, 12, 1, 19628, 1, 4178, 9, 519, 45, 3, 22, 543, 4, 4, 5, 9, 7, 3, 6, 31, 3, 149, 2, 1418, 49, 513, 54, 5, 49, 9, 0, 15, 0, 23, 4, 2, 14, 1361, 6, 2, 16, 3, 6, 2, 1, 2, 4, 101, 0, 161, 6, 10, 9, 357, 0, 62, 13, 499, 13, 245, 1, 2, 9, 233, 0, 3, 0, 8, 1, 6, 0, 475, 6, 110, 6, 6, 9, 4759, 9, 787719, 239];
function isInAstralSet(code, set) {
let pos = 0x10000;
for (let i = 0, length = set.length; i < length; i += 2) {
pos += set[i];
if (pos > code) return false;
pos += set[i + 1];
if (pos >= code) return true;
}
return false;
}
function isIdentifierStart(code) {
if (code < 65) return code === 36;
if (code <= 90) return true;
if (code < 97) return code === 95;
if (code <= 122) return true;
if (code <= 0xffff) {
return code >= 0xaa && nonASCIIidentifierStart.test(String.fromCharCode(code));
}
return isInAstralSet(code, astralIdentifierStartCodes);
}
function isIdentifierChar(code) {
if (code < 48) return code === 36;
if (code < 58) return true;
if (code < 65) return false;
if (code <= 90) return true;
if (code < 97) return code === 95;
if (code <= 122) return true;
if (code <= 0xffff) {
return code >= 0xaa && nonASCIIidentifier.test(String.fromCharCode(code));
}
return isInAstralSet(code, astralIdentifierStartCodes) || isInAstralSet(code, astralIdentifierCodes);
}
function isIdentifierName(name) {
let isFirst = true;
for (let i = 0; i < name.length; i++) {
let cp = name.charCodeAt(i);
if ((cp & 0xfc00) === 0xd800 && i + 1 < name.length) {
const trail = name.charCodeAt(++i);
if ((trail & 0xfc00) === 0xdc00) {
cp = 0x10000 + ((cp & 0x3ff) << 10) + (trail & 0x3ff);
}
}
if (isFirst) {
isFirst = false;
if (!isIdentifierStart(cp)) {
return false;
}
} else if (!isIdentifierChar(cp)) {
return false;
}
}
return !isFirst;
}
//# sourceMappingURL=identifier.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,57 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "isIdentifierChar", {
enumerable: true,
get: function () {
return _identifier.isIdentifierChar;
}
});
Object.defineProperty(exports, "isIdentifierName", {
enumerable: true,
get: function () {
return _identifier.isIdentifierName;
}
});
Object.defineProperty(exports, "isIdentifierStart", {
enumerable: true,
get: function () {
return _identifier.isIdentifierStart;
}
});
Object.defineProperty(exports, "isKeyword", {
enumerable: true,
get: function () {
return _keyword.isKeyword;
}
});
Object.defineProperty(exports, "isReservedWord", {
enumerable: true,
get: function () {
return _keyword.isReservedWord;
}
});
Object.defineProperty(exports, "isStrictBindOnlyReservedWord", {
enumerable: true,
get: function () {
return _keyword.isStrictBindOnlyReservedWord;
}
});
Object.defineProperty(exports, "isStrictBindReservedWord", {
enumerable: true,
get: function () {
return _keyword.isStrictBindReservedWord;
}
});
Object.defineProperty(exports, "isStrictReservedWord", {
enumerable: true,
get: function () {
return _keyword.isStrictReservedWord;
}
});
var _identifier = require("./identifier.js");
var _keyword = require("./keyword.js");
//# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
{"version":3,"names":["_identifier","require","_keyword"],"sources":["../src/index.ts"],"sourcesContent":["export {\n isIdentifierName,\n isIdentifierChar,\n isIdentifierStart,\n} from \"./identifier.ts\";\nexport {\n isReservedWord,\n isStrictBindOnlyReservedWord,\n isStrictBindReservedWord,\n isStrictReservedWord,\n isKeyword,\n} from \"./keyword.ts\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,IAAAA,WAAA,GAAAC,OAAA;AAKA,IAAAC,QAAA,GAAAD,OAAA","ignoreList":[]}
@@ -0,0 +1,35 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.isKeyword = isKeyword;
exports.isReservedWord = isReservedWord;
exports.isStrictBindOnlyReservedWord = isStrictBindOnlyReservedWord;
exports.isStrictBindReservedWord = isStrictBindReservedWord;
exports.isStrictReservedWord = isStrictReservedWord;
const reservedWords = {
keyword: ["break", "case", "catch", "continue", "debugger", "default", "do", "else", "finally", "for", "function", "if", "return", "switch", "throw", "try", "var", "const", "while", "with", "new", "this", "super", "class", "extends", "export", "import", "null", "true", "false", "in", "instanceof", "typeof", "void", "delete"],
strict: ["implements", "interface", "let", "package", "private", "protected", "public", "static", "yield"],
strictBind: ["eval", "arguments"]
};
const keywords = new Set(reservedWords.keyword);
const reservedWordsStrictSet = new Set(reservedWords.strict);
const reservedWordsStrictBindSet = new Set(reservedWords.strictBind);
function isReservedWord(word, inModule) {
return inModule && word === "await" || word === "enum";
}
function isStrictReservedWord(word, inModule) {
return isReservedWord(word, inModule) || reservedWordsStrictSet.has(word);
}
function isStrictBindOnlyReservedWord(word) {
return reservedWordsStrictBindSet.has(word);
}
function isStrictBindReservedWord(word, inModule) {
return isStrictReservedWord(word, inModule) || isStrictBindOnlyReservedWord(word);
}
function isKeyword(word) {
return keywords.has(word);
}
//# sourceMappingURL=keyword.js.map
@@ -0,0 +1 @@
{"version":3,"names":["reservedWords","keyword","strict","strictBind","keywords","Set","reservedWordsStrictSet","reservedWordsStrictBindSet","isReservedWord","word","inModule","isStrictReservedWord","has","isStrictBindOnlyReservedWord","isStrictBindReservedWord","isKeyword"],"sources":["../src/keyword.ts"],"sourcesContent":["const reservedWords = {\n keyword: [\n \"break\",\n \"case\",\n \"catch\",\n \"continue\",\n \"debugger\",\n \"default\",\n \"do\",\n \"else\",\n \"finally\",\n \"for\",\n \"function\",\n \"if\",\n \"return\",\n \"switch\",\n \"throw\",\n \"try\",\n \"var\",\n \"const\",\n \"while\",\n \"with\",\n \"new\",\n \"this\",\n \"super\",\n \"class\",\n \"extends\",\n \"export\",\n \"import\",\n \"null\",\n \"true\",\n \"false\",\n \"in\",\n \"instanceof\",\n \"typeof\",\n \"void\",\n \"delete\",\n ],\n strict: [\n \"implements\",\n \"interface\",\n \"let\",\n \"package\",\n \"private\",\n \"protected\",\n \"public\",\n \"static\",\n \"yield\",\n ],\n strictBind: [\"eval\", \"arguments\"],\n};\nconst keywords = new Set(reservedWords.keyword);\nconst reservedWordsStrictSet = new Set(reservedWords.strict);\nconst reservedWordsStrictBindSet = new Set(reservedWords.strictBind);\n\n/**\n * Checks if word is a reserved word in non-strict mode\n */\nexport function isReservedWord(word: string, inModule: boolean): boolean {\n return (inModule && word === \"await\") || word === \"enum\";\n}\n\n/**\n * Checks if word is a reserved word in non-binding strict mode\n *\n * Includes non-strict reserved words\n */\nexport function isStrictReservedWord(word: string, inModule: boolean): boolean {\n return isReservedWord(word, inModule) || reservedWordsStrictSet.has(word);\n}\n\n/**\n * Checks if word is a reserved word in binding strict mode, but it is allowed as\n * a normal identifier.\n */\nexport function isStrictBindOnlyReservedWord(word: string): boolean {\n return reservedWordsStrictBindSet.has(word);\n}\n\n/**\n * Checks if word is a reserved word in binding strict mode\n *\n * Includes non-strict reserved words and non-binding strict reserved words\n */\nexport function isStrictBindReservedWord(\n word: string,\n inModule: boolean,\n): boolean {\n return (\n isStrictReservedWord(word, inModule) || isStrictBindOnlyReservedWord(word)\n );\n}\n\nexport function isKeyword(word: string): boolean {\n return keywords.has(word);\n}\n"],"mappings":";;;;;;;;;;AAAA,MAAMA,aAAa,GAAG;EACpBC,OAAO,EAAE,CACP,OAAO,EACP,MAAM,EACN,OAAO,EACP,UAAU,EACV,UAAU,EACV,SAAS,EACT,IAAI,EACJ,MAAM,EACN,SAAS,EACT,KAAK,EACL,UAAU,EACV,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,KAAK,EACL,KAAK,EACL,OAAO,EACP,OAAO,EACP,MAAM,EACN,KAAK,EACL,MAAM,EACN,OAAO,EACP,OAAO,EACP,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,MAAM,EACN,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,MAAM,EACN,QAAQ,CACT;EACDC,MAAM,EAAE,CACN,YAAY,EACZ,WAAW,EACX,KAAK,EACL,SAAS,EACT,SAAS,EACT,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,OAAO,CACR;EACDC,UAAU,EAAE,CAAC,MAAM,EAAE,WAAW;AAClC,CAAC;AACD,MAAMC,QAAQ,GAAG,IAAIC,GAAG,CAACL,aAAa,CAACC,OAAO,CAAC;AAC/C,MAAMK,sBAAsB,GAAG,IAAID,GAAG,CAACL,aAAa,CAACE,MAAM,CAAC;AAC5D,MAAMK,0BAA0B,GAAG,IAAIF,GAAG,CAACL,aAAa,CAACG,UAAU,CAAC;AAK7D,SAASK,cAAcA,CAACC,IAAY,EAAEC,QAAiB,EAAW;EACvE,OAAQA,QAAQ,IAAID,IAAI,KAAK,OAAO,IAAKA,IAAI,KAAK,MAAM;AAC1D;AAOO,SAASE,oBAAoBA,CAACF,IAAY,EAAEC,QAAiB,EAAW;EAC7E,OAAOF,cAAc,CAACC,IAAI,EAAEC,QAAQ,CAAC,IAAIJ,sBAAsB,CAACM,GAAG,CAACH,IAAI,CAAC;AAC3E;AAMO,SAASI,4BAA4BA,CAACJ,IAAY,EAAW;EAClE,OAAOF,0BAA0B,CAACK,GAAG,CAACH,IAAI,CAAC;AAC7C;AAOO,SAASK,wBAAwBA,CACtCL,IAAY,EACZC,QAAiB,EACR;EACT,OACEC,oBAAoB,CAACF,IAAI,EAAEC,QAAQ,CAAC,IAAIG,4BAA4B,CAACJ,IAAI,CAAC;AAE9E;AAEO,SAASM,SAASA,CAACN,IAAY,EAAW;EAC/C,OAAOL,QAAQ,CAACQ,GAAG,CAACH,IAAI,CAAC;AAC3B","ignoreList":[]}
@@ -0,0 +1,31 @@
{
"name": "@babel/helper-validator-identifier",
"version": "7.29.7",
"description": "Validate identifier/keywords name",
"repository": {
"type": "git",
"url": "https://github.com/babel/babel.git",
"directory": "packages/babel-helper-validator-identifier"
},
"license": "MIT",
"publishConfig": {
"access": "public"
},
"main": "./lib/index.js",
"exports": {
".": {
"types": "./lib/index.d.ts",
"default": "./lib/index.js"
},
"./package.json": "./package.json"
},
"devDependencies": {
"@unicode/unicode-17.0.0": "^1.6.10",
"charcodes": "^0.2.0"
},
"engines": {
"node": ">=6.9.0"
},
"author": "The Babel Team (https://babel.dev/team)",
"type": "commonjs"
}
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
Copyright (C) 2012-2014 by various contributors (see AUTHORS)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+19
View File
@@ -0,0 +1,19 @@
# @babel/parser
> A JavaScript parser
See our website [@babel/parser](https://babeljs.io/docs/babel-parser) for more information or the [issues](https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A%20parser%22+is%3Aopen) associated with this package.
## Install
Using npm:
```sh
npm install --save-dev @babel/parser
```
or using yarn:
```sh
yarn add @babel/parser --dev
```
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env node
/* eslint-disable no-var, unicorn/prefer-node-protocol */
var parser = require("..");
var fs = require("fs");
var filename = process.argv[2];
if (!filename) {
console.error("no filename specified");
} else {
var file = fs.readFileSync(filename, "utf8");
var ast = parser.parse(file);
console.log(JSON.stringify(ast, null, " "));
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+50
View File
@@ -0,0 +1,50 @@
{
"name": "@babel/parser",
"version": "7.29.8",
"description": "A JavaScript parser",
"author": "The Babel Team (https://babel.dev/team)",
"homepage": "https://babel.dev/docs/en/next/babel-parser",
"bugs": "https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A+parser+%28babylon%29%22+is%3Aopen",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"keywords": [
"babel",
"javascript",
"parser",
"tc39",
"ecmascript",
"@babel/parser"
],
"repository": {
"type": "git",
"url": "https://github.com/babel/babel.git",
"directory": "packages/babel-parser"
},
"main": "./lib/index.js",
"types": "./typings/babel-parser.d.ts",
"files": [
"bin",
"lib",
"typings/babel-parser.d.ts",
"index.cjs"
],
"engines": {
"node": ">=6.0.0"
},
"# dependencies": "This package doesn't actually have runtime dependencies. @babel/types is only needed for type definitions.",
"dependencies": {
"@babel/types": "^7.29.8"
},
"devDependencies": {
"@babel/code-frame": "^7.29.7",
"@babel/helper-check-duplicate-nodes": "^7.29.7",
"@babel/helper-fixtures": "^7.29.7",
"@babel/helper-string-parser": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7",
"charcodes": "^0.2.0"
},
"bin": "./bin/babel-parser.js",
"type": "commonjs"
}
@@ -0,0 +1,262 @@
// This file is auto-generated! Do not modify it directly.
// Run `yarn gulp bundle-dts` to re-generate it.
/* eslint-disable @typescript-eslint/consistent-type-imports, @typescript-eslint/no-redundant-type-constituents */
import { File, Expression } from '@babel/types';
declare class Position {
line: number;
column: number;
index: number;
constructor(line: number, col: number, index: number);
}
type SyntaxPlugin = "flow" | "typescript" | "jsx" | "functionBind" | "pipelineOperator" | "placeholders";
type ParseErrorCode = "BABEL_PARSER_SYNTAX_ERROR" | "BABEL_PARSER_SOURCETYPE_MODULE_REQUIRED";
interface ParseErrorSpecification<ErrorDetails> {
code: ParseErrorCode;
reasonCode: string;
syntaxPlugin?: SyntaxPlugin;
missingPlugin?: string | string[];
loc: Position;
details: ErrorDetails;
pos: number;
}
type ParseError$1<ErrorDetails> = SyntaxError & ParseErrorSpecification<ErrorDetails>;
type BABEL_8_BREAKING = false;
type IF_BABEL_7<V> = false extends BABEL_8_BREAKING ? V : never;
type Plugin$1 =
| "asyncDoExpressions"
| IF_BABEL_7<"asyncGenerators">
| IF_BABEL_7<"bigInt">
| IF_BABEL_7<"classPrivateMethods">
| IF_BABEL_7<"classPrivateProperties">
| IF_BABEL_7<"classProperties">
| IF_BABEL_7<"classStaticBlock">
| IF_BABEL_7<"decimal">
| "decorators-legacy"
| "deferredImportEvaluation"
| "decoratorAutoAccessors"
| "destructuringPrivate"
| IF_BABEL_7<"deprecatedImportAssert">
| "doExpressions"
| IF_BABEL_7<"dynamicImport">
| IF_BABEL_7<"explicitResourceManagement">
| "exportDefaultFrom"
| IF_BABEL_7<"exportNamespaceFrom">
| "flow"
| "flowComments"
| "functionBind"
| "functionSent"
| "importMeta"
| "jsx"
| IF_BABEL_7<"jsonStrings">
| IF_BABEL_7<"logicalAssignment">
| IF_BABEL_7<"importAssertions">
| IF_BABEL_7<"importReflection">
| "moduleBlocks"
| IF_BABEL_7<"moduleStringNames">
| IF_BABEL_7<"nullishCoalescingOperator">
| IF_BABEL_7<"numericSeparator">
| IF_BABEL_7<"objectRestSpread">
| IF_BABEL_7<"optionalCatchBinding">
| IF_BABEL_7<"optionalChaining">
| "partialApplication"
| "placeholders"
| IF_BABEL_7<"privateIn">
| IF_BABEL_7<"regexpUnicodeSets">
| "sourcePhaseImports"
| "throwExpressions"
| IF_BABEL_7<"topLevelAwait">
| "v8intrinsic"
| ParserPluginWithOptions[0];
type ParserPluginWithOptions =
| ["decorators", DecoratorsPluginOptions]
| ["discardBinding", { syntaxType: "void" }]
| ["estree", { classFeatures?: boolean }]
| IF_BABEL_7<["importAttributes", { deprecatedAssertSyntax: boolean }]>
| IF_BABEL_7<["moduleAttributes", { version: "may-2020" }]>
| ["optionalChainingAssign", { version: "2023-07" }]
| ["pipelineOperator", PipelineOperatorPluginOptions]
| ["recordAndTuple", RecordAndTuplePluginOptions]
| ["flow", FlowPluginOptions]
| ["typescript", TypeScriptPluginOptions];
type PluginConfig = Plugin$1 | ParserPluginWithOptions;
interface DecoratorsPluginOptions {
decoratorsBeforeExport?: boolean;
allowCallParenthesized?: boolean;
}
interface PipelineOperatorPluginOptions {
proposal: BABEL_8_BREAKING extends false
? "minimal" | "fsharp" | "hack" | "smart"
: "fsharp" | "hack";
topicToken?: "%" | "#" | "@@" | "^^" | "^";
}
interface RecordAndTuplePluginOptions {
syntaxType: "bar" | "hash";
}
type FlowPluginOptions = BABEL_8_BREAKING extends true
? {
all?: boolean;
enums?: boolean;
}
: {
all?: boolean;
};
interface TypeScriptPluginOptions {
dts?: boolean;
disallowAmbiguousJSXLike?: boolean;
}
type Plugin = PluginConfig;
type SourceType = "script" | "commonjs" | "module" | "unambiguous";
interface Options {
/**
* By default, import and export declarations can only appear at a program's top level.
* Setting this option to true allows them anywhere where a statement is allowed.
*/
allowImportExportEverywhere?: boolean;
/**
* By default, await use is not allowed outside of an async function.
* Set this to true to accept such code.
*/
allowAwaitOutsideFunction?: boolean;
/**
* By default, a return statement at the top level raises an error.
* Set this to true to accept such code.
*/
allowReturnOutsideFunction?: boolean;
/**
* By default, new.target use is not allowed outside of a function or class.
* Set this to true to accept such code.
*/
allowNewTargetOutsideFunction?: boolean;
/**
* By default, super calls are not allowed outside of a method.
* Set this to true to accept such code.
*/
allowSuperOutsideMethod?: boolean;
/**
* By default, exported identifiers must refer to a declared variable.
* Set this to true to allow export statements to reference undeclared variables.
*/
allowUndeclaredExports?: boolean;
/**
* By default, yield use is not allowed outside of a generator function.
* Set this to true to accept such code.
*/
allowYieldOutsideFunction?: boolean;
/**
* By default, Babel parser JavaScript code according to Annex B syntax.
* Set this to `false` to disable such behavior.
*/
annexB?: boolean;
/**
* By default, Babel attaches comments to adjacent AST nodes.
* When this option is set to false, comments are not attached.
* It can provide up to 30% performance improvement when the input code has many comments.
* @babel/eslint-parser will set it for you.
* It is not recommended to use attachComment: false with Babel transform,
* as doing so removes all the comments in output code, and renders annotations such as
* /* istanbul ignore next *\/ nonfunctional.
*/
attachComment?: boolean;
/**
* By default, Babel always throws an error when it finds some invalid code.
* When this option is set to true, it will store the parsing error and
* try to continue parsing the invalid input file.
*/
errorRecovery?: boolean;
/**
* Indicate the mode the code should be parsed in.
* Can be one of "script", "commonjs", "module", or "unambiguous". Defaults to "script".
* "unambiguous" will make @babel/parser attempt to guess, based on the presence
* of ES6 import or export statements.
* Files with ES6 imports and exports are considered "module" and are otherwise "script".
*
* Use "commonjs" to parse code that is intended to be run in a CommonJS environment such as Node.js.
*/
sourceType?: SourceType;
/**
* Correlate output AST nodes with their source filename.
* Useful when generating code and source maps from the ASTs of multiple input files.
*/
sourceFilename?: string;
/**
* By default, all source indexes start from 0.
* You can provide a start index to alternatively start with.
* Useful for integration with other source tools.
*/
startIndex?: number;
/**
* By default, the first line of code parsed is treated as line 1.
* You can provide a line number to alternatively start with.
* Useful for integration with other source tools.
*/
startLine?: number;
/**
* By default, the parsed code is treated as if it starts from line 1, column 0.
* You can provide a column number to alternatively start with.
* Useful for integration with other source tools.
*/
startColumn?: number;
/**
* Array containing the plugins that you want to enable.
*/
plugins?: Plugin[];
/**
* Should the parser work in strict mode.
* Defaults to true if sourceType === 'module'. Otherwise, false.
*/
strictMode?: boolean;
/**
* Adds a ranges property to each node: [node.start, node.end]
*/
ranges?: boolean;
/**
* Adds all parsed tokens to a tokens property on the File node.
*/
tokens?: boolean;
/**
* By default, the parser adds information about parentheses by setting
* `extra.parenthesized` to `true` as needed.
* When this option is `true` the parser creates `ParenthesizedExpression`
* AST nodes instead of using the `extra` property.
*/
createParenthesizedExpressions?: boolean;
/**
* The default is false in Babel 7 and true in Babel 8
* Set this to true to parse it as an `ImportExpression` node.
* Otherwise `import(foo)` is parsed as `CallExpression(Import, [Identifier(foo)])`.
*/
createImportExpressions?: boolean;
}
type ParserOptions = Partial<Options>;
type ParseError = ParseError$1<object>;
type ParseResult<Result extends File | Expression = File> = Result & {
comments: File["comments"];
errors: null | ParseError[];
tokens?: File["tokens"];
};
/**
* Parse the provided code as an entire ECMAScript program.
*/
declare function parse(input: string, options?: ParserOptions): ParseResult<File>;
declare function parseExpression(input: string, options?: ParserOptions): ParseResult<Expression>;
declare const tokTypes: {
// todo(flow->ts) real token type
[name: string]: any;
};
export { DecoratorsPluginOptions, FlowPluginOptions, ParseError, ParseResult, ParserOptions, PluginConfig as ParserPlugin, ParserPluginWithOptions, PipelineOperatorPluginOptions, RecordAndTuplePluginOptions, TypeScriptPluginOptions, parse, parseExpression, tokTypes };
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+19
View File
@@ -0,0 +1,19 @@
# @babel/types
> Babel Types is a Lodash-esque utility library for AST nodes
See our website [@babel/types](https://babeljs.io/docs/babel-types) for more information or the [issues](https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A%20types%22+is%3Aopen) associated with this package.
## Install
Using npm:
```sh
npm install --save-dev @babel/types
```
or using yarn:
```sh
yarn add @babel/types --dev
```
@@ -0,0 +1,16 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = assertNode;
var _isNode = require("../validators/isNode.js");
function assertNode(node) {
if (!(0, _isNode.default)(node)) {
var _node$type;
const type = (_node$type = node == null ? void 0 : node.type) != null ? _node$type : JSON.stringify(node);
throw new TypeError(`Not a valid node of type "${type}"`);
}
}
//# sourceMappingURL=assertNode.js.map
@@ -0,0 +1 @@
{"version":3,"names":["_isNode","require","assertNode","node","isNode","_node$type","type","JSON","stringify","TypeError"],"sources":["../../src/asserts/assertNode.ts"],"sourcesContent":["import isNode from \"../validators/isNode.ts\";\nimport type * as t from \"../index.ts\";\n\nexport default function assertNode(node?: any): asserts node is t.Node {\n if (!isNode(node)) {\n const type = node?.type ?? JSON.stringify(node);\n throw new TypeError(`Not a valid node of type \"${type}\"`);\n }\n}\n"],"mappings":";;;;;;AAAA,IAAAA,OAAA,GAAAC,OAAA;AAGe,SAASC,UAAUA,CAACC,IAAU,EAA0B;EACrE,IAAI,CAAC,IAAAC,eAAM,EAACD,IAAI,CAAC,EAAE;IAAA,IAAAE,UAAA;IACjB,MAAMC,IAAI,IAAAD,UAAA,GAAGF,IAAI,oBAAJA,IAAI,CAAEG,IAAI,YAAAD,UAAA,GAAIE,IAAI,CAACC,SAAS,CAACL,IAAI,CAAC;IAC/C,MAAM,IAAIM,SAAS,CAAC,6BAA6BH,IAAI,GAAG,CAAC;EAC3D;AACF","ignoreList":[]}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
"use strict";
//# sourceMappingURL=index.js.map

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