feat(openclaw): add shopping, supermarket deals, geizhals, and ebay MCP tools
This commit is contained in:
+4
-1
@@ -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
|
||||||
@@ -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
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Machine & AI Assistant Authentication Guide
|
||||||
|
|
||||||
|
This guide describes how to configure programmatic, machine-to-machine, and AI assistant authentication across services protected by **Authentik** and **Traefik ForwardAuth**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Use Case
|
||||||
|
When an AI assistant (or automation script, CLI tool, or cron job) needs to:
|
||||||
|
* Inspect Zigbee2MQTT logs or device states via `https://zigbee2mqtt.moritzgraf.de`
|
||||||
|
* Scrape or query metrics from `https://prometheus.haumdaucher.de`
|
||||||
|
* Trigger or query APIs protected behind the Traefik ForwardAuth gateway
|
||||||
|
|
||||||
|
Instead of interactive browser logins or hardcoding static basic auth credentials into individual ingress configs, Authentik supports **Service Accounts & API Tokens**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Setup Instructions
|
||||||
|
|
||||||
|
### 1. Create a Service Account in Authentik
|
||||||
|
1. Navigate to **Directory > Users** in Authentik (`https://auth.haumdaucher.de`).
|
||||||
|
2. Click **Create Service Account**:
|
||||||
|
* **Username**: `ai-assistant` (or `mopbot`, `cli-agent`)
|
||||||
|
* **Create API Token**: Check this box.
|
||||||
|
* **Token Expiration**: Set to never expire (or desired TTL).
|
||||||
|
3. Copy the generated **Token Secret** (e.g. `ak_tok_xxxx...`).
|
||||||
|
|
||||||
|
### 2. Grant Access Permissions
|
||||||
|
1. Go to **Directory > Groups**.
|
||||||
|
2. Add the `ai-assistant` service account to the appropriate group (e.g. `infra-admins` or `zigbee-readers`).
|
||||||
|
3. Ensure the Application/Proxy Provider policy allows this group.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 Making Authenticated API Calls
|
||||||
|
|
||||||
|
### HTTP Bearer Token (Recommended)
|
||||||
|
Pass the API token in the `Authorization` header:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer <your-authentik-token>" \
|
||||||
|
https://zigbee2mqtt.moritzgraf.de/api/bridge/info
|
||||||
|
```
|
||||||
|
|
||||||
|
When Traefik ForwardAuth queries Authentik with this header, Authentik detects the Bearer token, authenticates the `ai-assistant` service account, checks permissions, and responds with HTTP `200 OK`. Traefik then proxies the request directly to the backend.
|
||||||
|
|
||||||
|
### Python / Scripting Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
AUTH_TOKEN = "your-authentik-token"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {AUTH_TOKEN}",
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get("https://zigbee2mqtt.moritzgraf.de/api/bridge/devices", headers=headers)
|
||||||
|
print(response.json())
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Rotating & Revoking Tokens
|
||||||
|
* To rotate a token, navigate to **Directory > Tokens and App passwords**.
|
||||||
|
* You can view active tokens, inspect last-used timestamps, and revoke compromised tokens instantly with one click without restarting any Kubernetes pods or modifying ingress secrets.
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
# Authentik Identity & Access Management (SSO)
|
||||||
|
|
||||||
|
Centralized authentication and identity provider for the `haumdaucher` Kubernetes cluster.
|
||||||
|
|
||||||
|
* **Domain**: `https://auth.haumdaucher.de`
|
||||||
|
* **Helm Chart**: `authentik/authentik` (Version 2026.8.0)
|
||||||
|
* **Database**: CloudNative-PG (`cnpg`) PostgreSQL cluster (`authentik-db`)
|
||||||
|
* **Ingress**: Traefik with Let's Encrypt TLS certificate (`auth-haumdaucher-de`)
|
||||||
|
* **Capabilities**: OIDC/OAuth2 Provider, Traefik ForwardAuth, 2FA (WebAuthn/Passkey, TOTP), User Invitations, Machine/AI Service Accounts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Architecture Layout
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
Client[Browser / User] -->|HTTPS| Traefik[Traefik Ingress]
|
||||||
|
AI[AI Assistant / CLI Script] -->|HTTPS + Bearer Token| Traefik
|
||||||
|
|
||||||
|
subgraph "ForwardAuth Ingresses"
|
||||||
|
Traefik -->|ForwardAuth Check| ForwardAuthMW[Traefik ForwardAuth Middleware]
|
||||||
|
ForwardAuthMW -->|Verify Session or Token| AuthServer[Authentik Server]
|
||||||
|
Traefik -->|Allow| Z2M[Zigbee2MQTT]
|
||||||
|
Traefik -->|Allow| Prom[Prometheus & Alertmanager]
|
||||||
|
Traefik -->|Allow| PMA[phpMyAdmin]
|
||||||
|
Traefik -->|Allow| OC[OpenClaw]
|
||||||
|
Traefik -->|Allow| N8N[n8n Web UI]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Native OIDC Workloads"
|
||||||
|
Grafana[Grafana] -->|OIDC Auth Code Flow| AuthServer
|
||||||
|
Coder[Coder v2] -->|OIDC Auth| AuthServer
|
||||||
|
Gitea[Gitea] -->|OIDC Auth| AuthServer
|
||||||
|
Nextcloud[Nextcloud user_oidc] -->|OIDC Auth| AuthServer
|
||||||
|
MinIO[MinIO Console] -->|OIDC Auth| AuthServer
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "auth Namespace"
|
||||||
|
AuthServer --> DB[(CNPG Postgres: authentik-db)]
|
||||||
|
AuthWorker[Authentik Worker] --> DB
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Deployment Instructions
|
||||||
|
|
||||||
|
### 1. Create Namespace & Provision CNPG Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create auth namespace
|
||||||
|
kubectl create namespace auth
|
||||||
|
|
||||||
|
# Apply CNPG PostgreSQL cluster
|
||||||
|
kubectl apply -f k8s/auth/authentik-db.yaml
|
||||||
|
|
||||||
|
# Verify PostgreSQL cluster is ready (Status: Cluster in healthy state)
|
||||||
|
kubectl get cluster -n auth authentik-db
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Deploy Authentik via Helm
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add Authentik repository
|
||||||
|
helm repo add authentik https://charts.goauthentik.io
|
||||||
|
helm repo update
|
||||||
|
|
||||||
|
# Install or upgrade Authentik release
|
||||||
|
helm upgrade --install authentik authentik/authentik \
|
||||||
|
--namespace auth \
|
||||||
|
-f k8s/auth/authentik.secret.values.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Deploy Traefik ForwardAuth Middleware
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -f k8s/auth/traefik-forwardauth-middleware.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 Initial Setup & Admin Onboarding
|
||||||
|
|
||||||
|
1. Open your browser and navigate to:
|
||||||
|
```
|
||||||
|
https://auth.haumdaucher.de/if/flow/initial-setup/
|
||||||
|
```
|
||||||
|
2. Set a secure password for the default `akadmin` account.
|
||||||
|
3. Configure your admin user profile and optionally enroll a WebAuthn / Passkey / TOTP 2FA device.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👥 User Management & Invitations
|
||||||
|
|
||||||
|
### Creating Users & Sending Invites
|
||||||
|
1. Navigate to **Directory > Users** in the Authentik Admin interface.
|
||||||
|
2. You can create users manually or upload/provision users in batches.
|
||||||
|
3. To send enrollment links, go to **Directory > Invitations**:
|
||||||
|
* Click **Create**.
|
||||||
|
* Set the target email and assign user groups (e.g. `family`, `friends`, `admins`).
|
||||||
|
* Copy the generated invitation URL or send it to the user.
|
||||||
|
|
||||||
|
### Group Permissions
|
||||||
|
* `authentik Admins`: Full administrative control over Authentik.
|
||||||
|
* `infra-admins`: Access to infrastructure tools (Prometheus, Alertmanager, phpMyAdmin, Zigbee2MQTT).
|
||||||
|
* `users`: Standard user group with access to shared web services (Grafana dashboards, Coder, Nextcloud, Gitea).
|
||||||
|
|
||||||
|
👉 **For the complete guide on Terraform user management, bulk Excel/CSV imports, custom email templates, and RBAC/PBAC access limiting, see [USER_MANAGEMENT.md](./USER_MANAGEMENT.md).**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤖 Machine & AI Agent Access (Programmatic Authentication)
|
||||||
|
|
||||||
|
For automated tools, scripts, or AI assistants that need to inspect protected applications (e.g. querying Zigbee2MQTT API/logs or Prometheus metrics):
|
||||||
|
|
||||||
|
1. **Create a Service Account**:
|
||||||
|
* In Authentik Admin, navigate to **Directory > Users**.
|
||||||
|
* Click **Create Service Account** (e.g. `ai-agent` or `automation-bot`).
|
||||||
|
* Create a permanent API token for the service account.
|
||||||
|
2. **Accessing Protected Endpoints**:
|
||||||
|
* Pass the token in the HTTP `Authorization` header:
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer <your-authentik-token>" https://zigbee2mqtt.moritzgraf.de/api/...
|
||||||
|
```
|
||||||
|
* Authentik's ForwardAuth middleware validates the Bearer token and allows the request without redirecting to a web login page.
|
||||||
|
|
||||||
|
See [MACHINE_AUTH.md](./MACHINE_AUTH.md) for full details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Protecting Applications (Rollout Guide)
|
||||||
|
|
||||||
|
### Pattern 1: Traefik ForwardAuth (Proxy Protection)
|
||||||
|
Use this for applications without native OIDC (e.g. Zigbee2MQTT, Prometheus, phpMyAdmin, OpenClaw).
|
||||||
|
|
||||||
|
1. In Authentik Admin, go to **Applications > Providers**:
|
||||||
|
* Create a **Proxy Provider**.
|
||||||
|
* Name: `<App Name> Proxy` (e.g. `Zigbee2MQTT Proxy`).
|
||||||
|
* Mode: `Forward auth (single application)`.
|
||||||
|
* External Host: `https://<app-domain>` (e.g. `https://zigbee2mqtt.moritzgraf.de`).
|
||||||
|
2. Go to **Applications > Applications**:
|
||||||
|
* Create Application, link the Provider, and set access policies/groups.
|
||||||
|
3. Add the middleware annotation to the application's Kubernetes Ingress:
|
||||||
|
```yaml
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/router.middlewares: auth-authentik-forwardauth@kubernetescrd
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Native OIDC SSO
|
||||||
|
Use this for applications supporting OpenID Connect (Grafana, Coder, Gitea, Nextcloud).
|
||||||
|
|
||||||
|
1. In Authentik Admin, create an **OAuth2/OpenID Provider**:
|
||||||
|
* Name: `<App Name> OIDC`
|
||||||
|
* Client Type: `Confidential`
|
||||||
|
* Redirect URI: e.g. `https://grafana.haumdaucher.de/login/generic_oauth`
|
||||||
|
2. Create an **Application** in Authentik and assign it to the Provider.
|
||||||
|
3. Configure the client credentials (`client_id` and `client_secret`) in the application's Helm values / ConfigMap.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Branding & Haumdaucher UI Theme
|
||||||
|
|
||||||
|
The Authentik login screen is customized with a **Haumdaucher** glassmorphism theme matching `haumdaucher.de`.
|
||||||
|
|
||||||
|
* **Styling Highlights**:
|
||||||
|
* **Glassmorphic Card**: Frosted glass panel (`backdrop-filter: blur(20px)`), smooth rounded corners (`24px`), and elegant ambient background aura.
|
||||||
|
* **Typography**: Clean, modern typography using Google Font **Outfit**.
|
||||||
|
* **Logo & Entrance Animation**: Dynamic 3D scale-and-fade entry animation for the Haumdaucher crest logo (`https://haumdaucher.de/images/logo_classic.png`).
|
||||||
|
* **Dual Light & Dark Theme**: Automatic adaptation with dark mode styling featuring obsidian surfaces, gold accents (`#d4af37`), and glowing tactile buttons.
|
||||||
|
* **Branded Footer**: "HAUMDAUCHER Wurst und Spezialitäten n.e.V." subtle watermark.
|
||||||
|
|
||||||
|
### Managing Theme via Terraform
|
||||||
|
Branding is provisioned via the `authentik_brand` resource in `terraform/authentik.tf` using the stylesheet `terraform/haumdaucher-theme.css`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd terraform
|
||||||
|
terraform apply -target=authentik_brand.haumdaucher
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💾 Backup & Disaster Recovery
|
||||||
|
|
||||||
|
* **Database**: Managed by CloudNative-PG on `openebs-hostpath` persistent storage.
|
||||||
|
* **Velero Integration**: The `auth` namespace is backed up during daily/weekly Velero schedules:
|
||||||
|
```bash
|
||||||
|
velero backup create auth-backup-$(date +%Y%m%d) --include-namespaces auth --wait
|
||||||
|
```
|
||||||
@@ -0,0 +1,629 @@
|
|||||||
|
# Authentik User Management: Complete Roundup & Operations Guide
|
||||||
|
|
||||||
|
This guide covers everything you need to know about managing users in Authentik for the **Haumdaucher** infrastructure (`https://auth.haumdaucher.de`): from manual creation and invitation workflows to Terraform IaC, custom email branding, fine-grained access control, handling custom attributes (e.g., Vereins-Mitgliedsnummer, second email), and bulk-importing users from an Excel / CSV spreadsheet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📑 Table of Contents
|
||||||
|
|
||||||
|
1. [Executive Summary: Quick Answers](#-executive-summary-quick-answers)
|
||||||
|
2. [Method 1: Manual Web UI & Invitation Links](#-method-1-manual-web-ui--invitation-links)
|
||||||
|
3. [Method 2: User Provisioning via Terraform](#-method-2-user-provisioning-via-terraform)
|
||||||
|
4. [Method 3: Bulk Import from Excel / CSV (Python Script)](#-method-3-bulk-import-from-excel--csv-python-script)
|
||||||
|
5. [Customizing Registration & Onboarding Emails](#-customizing-registration--onboarding-emails)
|
||||||
|
6. [Access Control & Authorization Policies (Limiting App Access)](#-access-control--authorization-policies-limiting-app-access)
|
||||||
|
7. [Custom Attributes & OIDC Claim Mappings](#-custom-attributes--oidc-claim-mappings)
|
||||||
|
8. [Production Checklist & Best Practices](#-production-checklist--best-practices)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ Executive Summary: Quick Answers
|
||||||
|
|
||||||
|
| Question | Answer & Key Mechanism |
|
||||||
|
| :--- | :--- |
|
||||||
|
| **How to add new users?** | **4 Ways**: (1) Admin Web UI (`Directory > Users`), (2) **Invitations flow** (`Directory > Invitations` - recommended for self-service), (3) **Terraform** (`authentik_user` resource), or (4) **REST API / Python script** for bulk imports. |
|
||||||
|
| **Can this be done in Terraform?** | **Yes.** Use `resource "authentik_user"` in `terraform/authentik.tf`. Supports custom attributes via `jsonencode()`, group assignments, and active status. |
|
||||||
|
| **Can I change the email sent to the user?** | **Yes.** Configured in **Flows & Stages > Stages > Email Stage**. You can customize the subject line, token expiration, and HTML template (written in Django template syntax with access to user fields and custom attributes). |
|
||||||
|
| **Can I limit what a user has access to?** | **Yes.** Through **Policy Bindings** attached to Applications: Group Membership Policies (e.g., `infra-admins` vs `users`), IP/Geo Policies, or Python Expression Policies checking custom attributes (e.g., only active club members). |
|
||||||
|
| **How to handle Excel tables with custom attributes?** | Authentik stores arbitrary key-value pairs in `user.attributes` (JSON). You can import an Excel/CSV file with a Python script or Terraform `csvdecode()`, map fields like `haumdaucher_mitgliedsnummer` or `second_email`, and expose them to downstream apps (Nextcloud, Grafana, Gitea) via OIDC Scope Mappings. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👤 Method 1: Manual Web UI & Invitation Links
|
||||||
|
|
||||||
|
Authentik offers two UI methods to onboard users:
|
||||||
|
|
||||||
|
### Option A: Direct User Creation (Admin sets password or triggers reset)
|
||||||
|
1. Go to `https://auth.haumdaucher.de` and log in with your admin account.
|
||||||
|
2. Navigate to **Directory > Users** and click **Create**.
|
||||||
|
3. Fill in:
|
||||||
|
* **Username**: `max.mustermann`
|
||||||
|
* **Name**: `Max Mustermann`
|
||||||
|
* **Email**: `max@example.com`
|
||||||
|
* **Type**: `Internal`
|
||||||
|
* **Groups**: Select groups (e.g., `users`, `verein-mitglieder`).
|
||||||
|
* **Attributes (YAML/JSON)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"second_email": "max.privat@gmail.com",
|
||||||
|
"haumdaucher_mitgliedsnummer": "HM-2024-042",
|
||||||
|
"phone": "+49 170 1234567"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. Click **Create**.
|
||||||
|
5. To set credentials: Click on the user -> **Actions** -> **Set Password** (or **Explicitly trigger password reset flow** so the user receives a link via email).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Troubleshooting: "Flow does not apply to current user"
|
||||||
|
|
||||||
|
If you encounter this screen when opening an invite link:
|
||||||
|
|
||||||
|
> **"Flow does not apply to current user."**
|
||||||
|
|
||||||
|
### Why this happens:
|
||||||
|
1. **Wrong Flow Selected (`default-source-enrollment`)**:
|
||||||
|
In your URL (`/if/flow/default-source-enrollment/?itoken=...`), the flow used was `default-source-enrollment`. That flow is strictly designed for external identity providers (OAuth/SAML like Google/GitHub) and binds a policy that checks for an active external source in the context. When accessed directly with an `?itoken=...`, Authentik evaluates this policy, fails, and displays this error.
|
||||||
|
2. **Missing Invitation Stage**:
|
||||||
|
Standard enrollment flows reject direct token access unless an **`Invitation Stage`** is bound to the beginning of the flow to validate `?itoken=...`.
|
||||||
|
3. **Active Admin Session**:
|
||||||
|
If an administrator is currently logged in to the browser and clicks the enrollment link, flows with an "unauthenticated-only" policy will reject the session. Always test invitation URLs in an **Incognito / Private window**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Solution: The Haumdaucher Custom Invitation Flow (`haumdaucher-invitation-flow`)
|
||||||
|
|
||||||
|
We provision a dedicated, streamlined flow in `terraform/authentik.tf`:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
Link[Invite Link with ?itoken=...] --> Stage1[1. Invitation Stage<br/>Validates Token]
|
||||||
|
Stage1 --> Stage2[2. Prompt Stage<br/>User inputs Username, Name, Password]
|
||||||
|
Stage2 --> Stage3[3. User Write Stage<br/>Writes to DB & assigns Groups]
|
||||||
|
Stage3 --> Stage4[4. User Login Stage<br/>Logs user into session]
|
||||||
|
Stage4 --> Stage5[5. Redirect Stage<br/>Redirects to haumdaucher.de]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Flow Behavior (Tailored to your setup):
|
||||||
|
* **User Inputs**: Prompts the user to choose their preferred **Username**, **Full Name**, and **Password** (with confirmation).
|
||||||
|
* **Email & Custom Attributes**: Pre-filled from the admin invitation (e.g. `haumdaucher_mitgliedsnummer`, `second_email`).
|
||||||
|
* **MFA / 2FA**: Optional. The user registers immediately with their password and can enroll Passkeys/TOTP anytime in their profile settings.
|
||||||
|
* **Automatic Redirect**: Upon completing registration, the user is automatically logged in and redirected to **`https://haumdaucher.de`**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### How to Create an Invitation using the Custom Flow
|
||||||
|
|
||||||
|
1. Go to `https://auth.haumdaucher.de` > **Directory > Invitations**.
|
||||||
|
2. Click **Create** (or **New Invitation**).
|
||||||
|
3. Set the fields:
|
||||||
|
* **Name**: `Invite - Max Mustermann`
|
||||||
|
* **Expires**: Set expiration (e.g., 7 days).
|
||||||
|
* **Flow**: Select **`Haumdaucher Invitation Flow`** (`haumdaucher-invitation-flow`).
|
||||||
|
* **Single use**: Checked (`true`).
|
||||||
|
* **Fixed Data / Attributes (JSON)**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "max@example.com",
|
||||||
|
"haumdaucher_mitgliedsnummer": "HM-2024-042",
|
||||||
|
"second_email": "max.privat@gmail.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. Click **Create**.
|
||||||
|
5. Copy the generated URL:
|
||||||
|
```
|
||||||
|
https://auth.haumdaucher.de/if/flow/haumdaucher-invitation-flow/?itoken=<generated-token>
|
||||||
|
```
|
||||||
|
6. Send the link to the user. When they open it, they will see the branded Haumdaucher registration form asking for their Username, Name, and Password!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Method 2: User Provisioning via Terraform
|
||||||
|
|
||||||
|
You can manage users, groups, and permissions directly in `terraform/authentik.tf`.
|
||||||
|
|
||||||
|
### 1. Single User Definition
|
||||||
|
```hcl
|
||||||
|
# Group for club members
|
||||||
|
resource "authentik_group" "verein_mitglieder" {
|
||||||
|
name = "verein-mitglieder"
|
||||||
|
is_superuser = false
|
||||||
|
}
|
||||||
|
|
||||||
|
# User with custom attributes
|
||||||
|
resource "authentik_user" "max_mustermann" {
|
||||||
|
username = "max.mustermann"
|
||||||
|
name = "Max Mustermann"
|
||||||
|
email = "max@example.com"
|
||||||
|
type = "internal"
|
||||||
|
is_active = true
|
||||||
|
groups = [
|
||||||
|
authentik_group.users.id,
|
||||||
|
authentik_group.verein_mitglieder.id
|
||||||
|
]
|
||||||
|
attributes = jsonencode({
|
||||||
|
"second_email" = "max.privat@gmail.com",
|
||||||
|
"haumdaucher_mitgliedsnummer" = "HM-2024-042",
|
||||||
|
"eintrittsdatum" = "2024-01-15",
|
||||||
|
"status" = "aktiv"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Batch Provisioning from CSV in Terraform
|
||||||
|
If you maintain users in a CSV file (`terraform/users.csv`):
|
||||||
|
|
||||||
|
```csv
|
||||||
|
username,name,email,second_email,mitgliedsnummer,is_admin
|
||||||
|
max.mustermann,Max Mustermann,max@example.com,max.privat@gmail.com,HM-2024-042,false
|
||||||
|
anna.schmidt,Anna Schmidt,anna@example.com,anna.work@company.de,HM-2024-043,true
|
||||||
|
```
|
||||||
|
|
||||||
|
In `terraform/authentik.tf`:
|
||||||
|
```hcl
|
||||||
|
locals {
|
||||||
|
csv_users = csvdecode(file("${path.module}/users.csv"))
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "authentik_user" "csv_members" {
|
||||||
|
for_each = { for u in local.csv_users : u.username => u }
|
||||||
|
|
||||||
|
username = each.value.username
|
||||||
|
name = each.value.name
|
||||||
|
email = each.value.email
|
||||||
|
type = "internal"
|
||||||
|
is_active = true
|
||||||
|
groups = compact([
|
||||||
|
authentik_group.users.id,
|
||||||
|
authentik_group.verein_mitglieder.id,
|
||||||
|
each.value.is_admin == "true" ? authentik_group.infra_admins.id : null
|
||||||
|
])
|
||||||
|
attributes = jsonencode({
|
||||||
|
"second_email" = each.value.second_email,
|
||||||
|
"haumdaucher_mitgliedsnummer" = each.value.mitgliedsnummer
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚖️ Trade-offs: When to use Terraform for Users
|
||||||
|
* **Pros**: Version-controlled in Git, declarative, reproducible, ideal for admins and service accounts (`ai_assistant`).
|
||||||
|
* **Cons / Considerations**: Terraform cannot trigger interactive password onboarding emails out of the box; passwords must be set via recovery flow or first login reset.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Method 3: Bulk Import from Excel / CSV (Python Script)
|
||||||
|
|
||||||
|
If you have an Excel file (`mitglieder.xlsx`) or CSV, using Authentik's REST API is the cleanest approach because it can **create users, attach custom attributes, and automatically email them an invitation link to set their own password**.
|
||||||
|
|
||||||
|
### Ready-to-Run Python Import Script
|
||||||
|
Save this script (e.g. `k8s/auth/scripts/import_users_excel.py`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Bulk User Importer for Authentik (auth.haumdaucher.de)
|
||||||
|
Reads an Excel (.xlsx) or CSV file and creates users with custom attributes and invitation links.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
AUTHENTIK_URL = os.getenv("AUTHENTIK_URL", "https://auth.haumdaucher.de")
|
||||||
|
AUTHENTIK_TOKEN = os.getenv("AUTHENTIK_TOKEN") # API Token (e.g. ai_assistant_token)
|
||||||
|
|
||||||
|
if not AUTHENTIK_TOKEN:
|
||||||
|
print("❌ Error: AUTHENTIK_TOKEN environment variable is required.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
HEADERS = {
|
||||||
|
"Authorization": f"Bearer {AUTHENTIK_TOKEN}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_or_create_group(group_name: str) -> str:
|
||||||
|
"""Fetch group ID or create if it doesn't exist."""
|
||||||
|
res = requests.get(f"{AUTHENTIK_URL}/api/v3/core/groups/?name={group_name}", headers=HEADERS)
|
||||||
|
res.raise_for_status()
|
||||||
|
results = res.json().get("results", [])
|
||||||
|
if results:
|
||||||
|
return results[0]["pk"]
|
||||||
|
|
||||||
|
# Create group
|
||||||
|
create_res = requests.post(f"{AUTHENTIK_URL}/api/v3/core/groups/", headers=HEADERS, json={"name": group_name})
|
||||||
|
create_res.raise_for_status()
|
||||||
|
return create_res.json()["pk"]
|
||||||
|
|
||||||
|
def import_users_from_file(file_path: str, send_invitation: bool = True):
|
||||||
|
# Read Excel or CSV
|
||||||
|
if file_path.endswith((".xlsx", ".xls")):
|
||||||
|
df = pd.read_excel(file_path)
|
||||||
|
else:
|
||||||
|
df = pd.read_csv(file_path)
|
||||||
|
|
||||||
|
# Standardize column names (lowercase & stripped)
|
||||||
|
df.columns = [c.strip().lower() for c in df.columns]
|
||||||
|
|
||||||
|
print(f"📋 Found {len(df)} rows to process.")
|
||||||
|
|
||||||
|
users_group_pk = get_or_create_group("users")
|
||||||
|
verein_group_pk = get_or_create_group("verein-mitglieder")
|
||||||
|
|
||||||
|
for idx, row in df.iterrows():
|
||||||
|
name = str(row.get("name", "")).strip()
|
||||||
|
email = str(row.get("email", "")).strip()
|
||||||
|
username = str(row.get("username", "")).strip() or email.split("@")[0].lower()
|
||||||
|
second_email = str(row.get("second_email", row.get("zweite_email", ""))).strip()
|
||||||
|
mitgliedsnummer = str(row.get("mitgliedsnummer", row.get("verein_mitgliedsnummer", ""))).strip()
|
||||||
|
|
||||||
|
if not email or email == "nan":
|
||||||
|
print(f"⚠️ Skipping row {idx+1}: missing email")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Prepare user payload with custom attributes
|
||||||
|
user_payload = {
|
||||||
|
"username": username,
|
||||||
|
"name": name if name != "nan" else username,
|
||||||
|
"email": email,
|
||||||
|
"type": "internal",
|
||||||
|
"is_active": True,
|
||||||
|
"groups": [users_group_pk, verein_group_pk],
|
||||||
|
"attributes": {
|
||||||
|
"second_email": second_email if second_email != "nan" else "",
|
||||||
|
"haumdaucher_mitgliedsnummer": mitgliedsnummer if mitgliedsnummer != "nan" else "",
|
||||||
|
"source": "excel_import"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1. Create or Update User in Authentik
|
||||||
|
res = requests.get(f"{AUTHENTIK_URL}/api/v3/core/users/?username={username}", headers=HEADERS)
|
||||||
|
res.raise_for_status()
|
||||||
|
existing = res.json().get("results", [])
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
user_pk = existing[0]["pk"]
|
||||||
|
patch_res = requests.patch(f"{AUTHENTIK_URL}/api/v3/core/users/{user_pk}/", headers=HEADERS, json=user_payload)
|
||||||
|
patch_res.raise_for_status()
|
||||||
|
print(f"🔄 Updated existing user: {username} ({email})")
|
||||||
|
else:
|
||||||
|
create_res = requests.post(f"{AUTHENTIK_URL}/api/v3/core/users/", headers=HEADERS, json=user_payload)
|
||||||
|
if not create_res.ok:
|
||||||
|
print(f"❌ Failed to create {username}: {create_res.text}")
|
||||||
|
continue
|
||||||
|
user_pk = create_res.json()["pk"]
|
||||||
|
print(f"✅ Created user: {username} ({email}) [ID: {user_pk}]")
|
||||||
|
|
||||||
|
# 2. Generate Invitation / Password Reset Link
|
||||||
|
if send_invitation:
|
||||||
|
invitation_payload = {
|
||||||
|
"name": f"Onboarding {username}",
|
||||||
|
"single_use": True,
|
||||||
|
"fixed_data": {
|
||||||
|
"email": email,
|
||||||
|
"username": username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inv_res = requests.post(f"{AUTHENTIK_URL}/api/v3/stages/invitation/invitations/", headers=HEADERS, json=invitation_payload)
|
||||||
|
if inv_res.ok:
|
||||||
|
inv_key = inv_res.json()["pk"]
|
||||||
|
link = f"{AUTHENTIK_URL}/if/flow/default-enrollment-flow/?itoken={inv_key}"
|
||||||
|
print(f" ✉️ Invitation Link: {link}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python3 import_users_excel.py <path-to-excel-or-csv>")
|
||||||
|
sys.exit(1)
|
||||||
|
import_users_from_file(sys.argv[1])
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✉️ Customizing Registration & Onboarding Emails
|
||||||
|
|
||||||
|
When an admin creates a user or triggers an enrollment/password recovery, Authentik sends transactional emails. You can customize their sender address, subject line, and full HTML appearance.
|
||||||
|
|
||||||
|
### 1. SMTP & Sender Settings
|
||||||
|
Authentik sends emails using the SMTP configuration defined in `k8s/auth/authentik.secret.values.yaml` or global environment variables:
|
||||||
|
* `AUTHENTIK_EMAIL__HOST`: `smtp.gmail.com` (or Google Workspace / OVH relay)
|
||||||
|
* `AUTHENTIK_EMAIL__PORT`: `587`
|
||||||
|
* `AUTHENTIK_EMAIL__USE_TLS`: `true`
|
||||||
|
* `AUTHENTIK_EMAIL__USERNAME`: `service@haumdaucher.de`
|
||||||
|
* `AUTHENTIK_EMAIL__PASSWORD`: `<app-password>`
|
||||||
|
* `AUTHENTIK_EMAIL__FROM`: `HAUMDAUCHER Identity <service@haumdaucher.de>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Customizing Email Stages in Authentik
|
||||||
|
Authentik controls outgoing emails via **Email Stages**:
|
||||||
|
1. Open Admin UI: **Flows & Stages > Stages**.
|
||||||
|
2. Locate the relevant email stage:
|
||||||
|
* `default-password-reset-email` (for password recovery & manual user activation)
|
||||||
|
* `default-enrollment-email` / `invitation-email` (for new user invitations)
|
||||||
|
* `default-user-settings-password-reset-email`
|
||||||
|
3. Click **Edit** on the stage:
|
||||||
|
* **Subject**: `Willkommen bei Haumdaucher e.V. – Aktiviere deinen Zugang`
|
||||||
|
* **Token Expiry**: `Minutes=1440` (24 hours) or `Minutes=10080` (7 days)
|
||||||
|
* **Template**: Select standard template or your custom template path.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Customizing the HTML Email Template (Django Syntax)
|
||||||
|
Authentik email templates use Django template syntax. Available variables in email templates include:
|
||||||
|
* `{{ user.name }}` / `{{ user.username }}` / `{{ user.email }}`
|
||||||
|
* `{{ user.attributes.haumdaucher_mitgliedsnummer }}`
|
||||||
|
* `{{ url }}` (The activation / password reset link)
|
||||||
|
* `{{ token_expires }}` (Expiry timestamp)
|
||||||
|
* `{{ brand.branding_title }}` (`HAUMDAUCHER`)
|
||||||
|
|
||||||
|
#### Example Custom Haumdaucher Welcome Template
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: #0d0d12; color: #f0f0f5; margin: 0; padding: 40px; }
|
||||||
|
.card { background: #181824; border: 1px solid rgba(212, 175, 55, 0.3); border-radius: 16px; max-width: 540px; margin: 0 auto; padding: 32px; box-shadow: 0 12px 30px rgba(0,0,0,0.5); }
|
||||||
|
.header { text-align: center; margin-bottom: 24px; }
|
||||||
|
.logo { height: 60px; }
|
||||||
|
h1 { color: #d4af37; font-size: 22px; margin-top: 12px; }
|
||||||
|
p { color: #c0c0d0; line-height: 1.6; font-size: 15px; }
|
||||||
|
.btn { display: block; text-align: center; background: linear-gradient(135deg, #d4af37, #aa8520); color: #000; font-weight: bold; text-decoration: none; padding: 14px 24px; border-radius: 10px; margin: 28px 0; }
|
||||||
|
.meta { background: rgba(255,255,255,0.05); border-radius: 8px; padding: 12px 16px; font-size: 13px; color: #a0a0b0; }
|
||||||
|
.footer { text-align: center; font-size: 12px; color: #606070; margin-top: 30px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<div class="header">
|
||||||
|
<img class="logo" src="https://haumdaucher.de/images/logo_classic.png" alt="Haumdaucher Logo" />
|
||||||
|
<h1>Willkommen beim Haumdaucher e.V.</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Hallo <strong>{{ user.name }}</strong>,</p>
|
||||||
|
<p>dein zentraler Zugang für die Haumdaucher Cloud & Services wurde eingerichtet.</p>
|
||||||
|
|
||||||
|
{% if user.attributes.haumdaucher_mitgliedsnummer %}
|
||||||
|
<div class="meta">
|
||||||
|
<strong>Mitgliedsnummer:</strong> {{ user.attributes.haumdaucher_mitgliedsnummer }}<br/>
|
||||||
|
<strong>Benutzername:</strong> {{ user.username }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p>Klicke auf den Button unten, um dein Passwort festzulegen und deinen Zugang zu aktivieren:</p>
|
||||||
|
|
||||||
|
<a class="btn" href="{{ url }}">Zugang aktivieren & Passwort setzen</a>
|
||||||
|
|
||||||
|
<p style="font-size: 12px; color: #888;">Dieser Link ist 7 Tage lang gültig. Falls du die Registrierung nicht angefordert hast, kannst du diese E-Mail ignorieren.</p>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
HAUMDAUCHER e.V. • <a href="https://haumdaucher.de" style="color: #d4af37;">haumdaucher.de</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Access Control & Authorization Policies (Limiting App Access)
|
||||||
|
|
||||||
|
By default, an application without an attached policy allows **any authenticated user** to access it. Authentik provides fine-grained Access Control (RBAC & PBAC) to restrict access per application.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph "Authentik Directory Groups"
|
||||||
|
Admin["👑 <b>infra-admins</b><br/>(Moritz & Wife / Full Admin)"]
|
||||||
|
Verein["🍺 <b>verein-mitglieder</b><br/>(Club Members)"]
|
||||||
|
Normal["👤 <b>users</b><br/>(Normal Authenticated Users)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Tier 1: Admin & Ops Tools (ForwardAuth / OIDC)"
|
||||||
|
Z2M["Zigbee2MQTT"]
|
||||||
|
Prom["Prometheus & Alertmanager"]
|
||||||
|
PMA["phpMyAdmin (Nextcloud & Gitea)"]
|
||||||
|
N8N["n8n Automation"]
|
||||||
|
MinIO["MinIO Console"]
|
||||||
|
Coder["Coder v2"]
|
||||||
|
OpenClaw["OpenClaw Engine"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Tier 2: Daily Shared Services (OIDC)"
|
||||||
|
HASS["Home Assistant"]
|
||||||
|
Grafana["Grafana Dashboards"]
|
||||||
|
Nextcloud["Nextcloud Cloud"]
|
||||||
|
Gitea["Gitea Git Platform"]
|
||||||
|
ClubWeb["haumdaucher.de"]
|
||||||
|
end
|
||||||
|
|
||||||
|
%% Admin Links (Full Superadmin access everywhere)
|
||||||
|
Admin ==>|Full Access| Z2M
|
||||||
|
Admin ==>|Full Access| OpenClaw
|
||||||
|
Admin ==>|Admin| HASS
|
||||||
|
Admin ==>|Admin| Grafana
|
||||||
|
Admin ==>|Admin| Nextcloud
|
||||||
|
Admin ==>|Admin| Gitea
|
||||||
|
Admin ==>|Admin| ClubWeb
|
||||||
|
|
||||||
|
%% Normal User Links (Restricted)
|
||||||
|
Normal -->|Viewer (Read-Only)| Grafana
|
||||||
|
Normal -->|Dashboard View| HASS
|
||||||
|
Normal -->|Standard User| Nextcloud
|
||||||
|
Normal -->|Developer| Gitea
|
||||||
|
|
||||||
|
%% Club Members Links
|
||||||
|
Verein -->|Member Portal| ClubWeb
|
||||||
|
Verein -->|Club Shared Folder| Nextcloud
|
||||||
|
Verein -->|Viewer (Read-Only)| Grafana
|
||||||
|
```
|
||||||
|
|
||||||
|
### Master Permission Matrix
|
||||||
|
|
||||||
|
| Application | Domain | Auth Type | `infra-admins` (Moritz & Wife) | `verein-mitglieder` (Club) | `users` (Normal Users) |
|
||||||
|
| :--- | :--- | :--- | :---: | :---: | :---: |
|
||||||
|
| **OpenClaw** | `openclaw.haumdaucher.de` | ForwardAuth | **Full Access** ✅ | ❌ *Denied* | ❌ *Denied* |
|
||||||
|
| **Zigbee2MQTT** | `zigbee2mqtt.moritzgraf.de` | ForwardAuth | **Full Access** ✅ | ❌ *Denied* | ❌ *Denied* |
|
||||||
|
| **Prometheus / Alertmanager** | `prometheus.haumdaucher.de` | ForwardAuth | **Full Access** ✅ | ❌ *Denied* | ❌ *Denied* |
|
||||||
|
| **phpMyAdmin (All DBs)** | `*.phpmyadmin.haumdaucher.de` | ForwardAuth | **Full Access** ✅ | ❌ *Denied* | ❌ *Denied* |
|
||||||
|
| **n8n** | `n8n.moritzgraf.de` | ForwardAuth | **Full Access** ✅ | ❌ *Denied* | ❌ *Denied* |
|
||||||
|
| **MinIO Console** | `minio-console.haumdaucher.de` | Native OIDC | **`consoleAdmin`** ✅ | ❌ *Denied* | ❌ *Denied* |
|
||||||
|
| **Coder v2** | `coder.moritzgraf.de` | Native OIDC | **`Owner / Admin`** ✅ | ❌ *Denied* | ❌ *Denied* |
|
||||||
|
| **Home Assistant** | `hass.moritzgraf.de` | Native OIDC | **`Administrator`** ✅ | ❌ *Denied* | **`Standard User`** |
|
||||||
|
| **Grafana** | `grafana.haumdaucher.de` | Native OIDC | **`Admin`** ✅ | **`Viewer` (Read-Only)** | **`Viewer` (Read-Only)** |
|
||||||
|
| **Nextcloud** | `cloud.haumdaucher.de` | Native OIDC | **`Admin`** ✅ | **`User` (Club Folder)** | **`User` (Standard)** |
|
||||||
|
| **Gitea** | `git.moritzgraf.de` | Native OIDC | **`Admin`** ✅ | ❌ / Member | **`Developer`** |
|
||||||
|
| **haumdaucher.de** | `haumdaucher.de` | ForwardAuth / OIDC | **`Admin`** ✅ | **`Member Access`** ✅ | ❌ *Denied* |
|
||||||
|
|
||||||
|
### Policy Bindings in Terraform
|
||||||
|
All policy bindings are enforced directly in `terraform/authentik.tf`:
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
# 1. Admin-Only Tools (Restricted strictly to infra-admins)
|
||||||
|
resource "authentik_policy_binding" "openclaw_admin_only" {
|
||||||
|
target = authentik_application.openclaw.uuid
|
||||||
|
group = authentik_group.infra_admins.id
|
||||||
|
order = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "authentik_policy_binding" "zigbee2mqtt_admin_only" {
|
||||||
|
target = authentik_application.zigbee2mqtt.uuid
|
||||||
|
group = authentik_group.infra_admins.id
|
||||||
|
order = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Shared & Member Applications (Accessible to users group)
|
||||||
|
resource "authentik_policy_binding" "grafana_users" {
|
||||||
|
target = authentik_application.grafana.uuid
|
||||||
|
group = authentik_group.users.id
|
||||||
|
order = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "authentik_policy_binding" "nextcloud_users" {
|
||||||
|
target = authentik_application.nextcloud.uuid
|
||||||
|
group = authentik_group.users.id
|
||||||
|
order = 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Attribute-Based Policy (Python Expression Policy)
|
||||||
|
You can write Python expressions that evaluate custom user attributes dynamically:
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
resource "authentik_policy_expression" "require_active_membership" {
|
||||||
|
name = "require-active-haumdaucher-membership"
|
||||||
|
expression = <<EOF
|
||||||
|
# Must have a valid Mitgliedsnummer and active status
|
||||||
|
mitgliedsnummer = request.user.attributes.get('haumdaucher_mitgliedsnummer')
|
||||||
|
status = request.user.attributes.get('status', 'aktiv')
|
||||||
|
|
||||||
|
if not mitgliedsnummer or not mitgliedsnummer.startswith('HM-'):
|
||||||
|
ak_message("Keine gültige Haumdaucher Mitgliedsnummer hinterlegt.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if status != 'aktiv':
|
||||||
|
ak_message("Deine Mitgliedschaft ist derzeit nicht aktiv.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Attach policy to Nextcloud application
|
||||||
|
resource "authentik_policy_binding" "nextcloud_active_members" {
|
||||||
|
target = authentik_application.nextcloud.uuid
|
||||||
|
policy = authentik_policy_expression.require_active_membership.id
|
||||||
|
order = 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏷️ Custom Attributes & OIDC Claim Mappings
|
||||||
|
|
||||||
|
Authentik allows storing any arbitrary JSON object inside `user.attributes`.
|
||||||
|
|
||||||
|
### 1. Recommended Attribute Schema for Haumdaucher
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"haumdaucher_mitgliedsnummer": "HM-2024-042",
|
||||||
|
"second_email": "max.privat@gmail.com",
|
||||||
|
"eintrittsdatum": "2024-01-15",
|
||||||
|
"verein_rolle": "Vorstand / Kassenwart / Mitglied",
|
||||||
|
"phone": "+49 170 1234567",
|
||||||
|
"telegram_handle": "@max_mustermann"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Exposing Custom Attributes via OIDC Claims (Terraform)
|
||||||
|
To pass `haumdaucher_mitgliedsnummer` or `second_email` to downstream applications like **Nextcloud**, **Grafana**, or **Gitea**:
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
# Define a custom Scope Mapping in Terraform
|
||||||
|
resource "authentik_property_mapping_provider_scope" "haumdaucher_profile" {
|
||||||
|
name = "Haumdaucher Extended Profile"
|
||||||
|
scope_name = "haumdaucher_profile"
|
||||||
|
description = "Provides Vereins-Mitgliedsnummer and secondary email to OIDC clients"
|
||||||
|
expression = <<EOF
|
||||||
|
return {
|
||||||
|
"haumdaucher_mitgliedsnummer": request.user.attributes.get("haumdaucher_mitgliedsnummer", ""),
|
||||||
|
"second_email": request.user.attributes.get("second_email", ""),
|
||||||
|
"verein_rolle": request.user.attributes.get("verein_rolle", "Mitglied")
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add this mapping to the OAuth2 provider (e.g. Nextcloud or Grafana)
|
||||||
|
resource "authentik_provider_oauth2" "nextcloud" {
|
||||||
|
name = "Nextcloud"
|
||||||
|
client_id = "nextcloud"
|
||||||
|
client_secret = random_password.nextcloud_oauth_client_secret.result
|
||||||
|
client_type = "confidential"
|
||||||
|
authorization_flow = data.authentik_flow.default_authorization_flow.id
|
||||||
|
invalidation_flow = data.authentik_flow.default_invalidation_flow.id
|
||||||
|
signing_key = data.authentik_certificate_key_pair.generated.id
|
||||||
|
|
||||||
|
property_mappings = [
|
||||||
|
data.authentik_property_mapping_provider_scope.openid.id,
|
||||||
|
data.authentik_property_mapping_provider_scope.email.id,
|
||||||
|
data.authentik_property_mapping_provider_scope.profile.id,
|
||||||
|
authentik_property_mapping_provider_scope.haumdaucher_profile.id, # <-- Custom scope
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛡️ Production Checklist & Best Practices
|
||||||
|
|
||||||
|
When managing users in production, keep the following considerations in mind:
|
||||||
|
|
||||||
|
### 1. Passwordless & Multi-Factor Authentication (MFA / 2FA)
|
||||||
|
* Enforce **WebAuthn / Passkeys (FIDO2)** or **TOTP (Authenticator App)** for users in the `infra-admins` group.
|
||||||
|
* In Authentik, this is done by binding an Authenticator Validation Stage to the default authentication flow or creating an Admin Authorization Flow.
|
||||||
|
|
||||||
|
### 2. User Self-Service & Attribute Editing
|
||||||
|
* Authentik provides a self-service portal at `https://auth.haumdaucher.de/if/user/#/settings`.
|
||||||
|
* Users can change their password, register Passkeys, and edit user profile fields.
|
||||||
|
* **Security Rule**: Keep administrative attributes (like `haumdaucher_mitgliedsnummer`) read-only by not exposing them to the user edit flow, while allowing users to freely edit personal contact details like `second_email` or `phone`.
|
||||||
|
|
||||||
|
### 3. Offboarding & Member Deactivation
|
||||||
|
* If a member leaves the club:
|
||||||
|
1. Set `is_active = false` (or remove from `verein-mitglieder` group).
|
||||||
|
2. Authentik immediately terminates active sessions and revokes ForwardAuth access and OIDC refresh tokens.
|
||||||
|
3. No need to manually reset passwords on Nextcloud, Grafana, or Gitea—SSO handles deprovisioning instantly.
|
||||||
|
|
||||||
|
### 4. GDPR / DSGVO & Privacy
|
||||||
|
* Store only necessary data in user attributes.
|
||||||
|
* If a member requests account deletion, deleting the user in Authentik cascades identity termination across all connected services.
|
||||||
|
|
||||||
|
### 5. Backups & Audit Logs
|
||||||
|
* User database changes are stored in the PostgreSQL cluster (`authentik-db`) and backed up with daily Velero snapshot schedules.
|
||||||
|
* Authentik logs all login attempts, failed logins, and administrative modifications under **Events > Logs**.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: postgresql.cnpg.io/v1
|
||||||
|
kind: Cluster
|
||||||
|
metadata:
|
||||||
|
name: authentik-db
|
||||||
|
namespace: auth
|
||||||
|
spec:
|
||||||
|
instances: 1
|
||||||
|
bootstrap:
|
||||||
|
initdb:
|
||||||
|
database: authentik
|
||||||
|
owner: authentik
|
||||||
|
postgresql:
|
||||||
|
parameters:
|
||||||
|
shared_buffers: "128MB"
|
||||||
|
max_connections: "100"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 512Mi
|
||||||
|
limits:
|
||||||
|
memory: 2Gi
|
||||||
|
storage:
|
||||||
|
storageClass: openebs-hostpath
|
||||||
|
size: 5Gi
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# --- Authentik Helm Configuration ---
|
||||||
|
# Uses CloudNative-PG cluster (authentik-db) for PostgreSQL
|
||||||
|
|
||||||
|
global:
|
||||||
|
env:
|
||||||
|
- name: AUTHENTIK_POSTGRESQL__HOST
|
||||||
|
value: "authentik-db-rw.auth.svc.cluster.local"
|
||||||
|
- name: AUTHENTIK_POSTGRESQL__NAME
|
||||||
|
value: "authentik"
|
||||||
|
- name: AUTHENTIK_POSTGRESQL__USER
|
||||||
|
value: "authentik"
|
||||||
|
- name: AUTHENTIK_POSTGRESQL__PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: authentik-db-app
|
||||||
|
key: password
|
||||||
|
|
||||||
|
authentik:
|
||||||
|
# Secret key for cookie signing and session encryption
|
||||||
|
secret_key: "gU2ohe8eighaiph4quaeCh1ieM3iechoh2ohNg5yahz6eephoh"
|
||||||
|
error_reporting:
|
||||||
|
enabled: false
|
||||||
|
log_level: info
|
||||||
|
|
||||||
|
server:
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 384Mi
|
||||||
|
limits:
|
||||||
|
memory: 768Mi
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
ingressClassName: "traefik"
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: "traefik"
|
||||||
|
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||||
|
kubernetes.io/tls-acme: "true"
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||||
|
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||||
|
hosts:
|
||||||
|
- auth.haumdaucher.de
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- auth.haumdaucher.de
|
||||||
|
secretName: auth-haumdaucher-de
|
||||||
|
|
||||||
|
worker:
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 384Mi
|
||||||
|
limits:
|
||||||
|
memory: 768Mi
|
||||||
|
|
||||||
|
# Disable bundled subchart databases (managed by CNPG)
|
||||||
|
postgresql:
|
||||||
|
enabled: false
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# # status: removed again
|
|
||||||
|
|
||||||
# apiVersion: helm.fluxcd.io/v1
|
|
||||||
# kind: HelmRelease
|
|
||||||
# metadata:
|
|
||||||
# name: helm-phpldapadmin
|
|
||||||
# namespace: flux
|
|
||||||
# spec:
|
|
||||||
# releaseName: phpldapadmin
|
|
||||||
# targetNamespace: auth
|
|
||||||
# chart:
|
|
||||||
# repository: https://cetic.github.io/helm-charts
|
|
||||||
# version: 0.1.3
|
|
||||||
# name: phpldapadmin
|
|
||||||
# values:
|
|
||||||
# service:
|
|
||||||
# type: ClusterIP
|
|
||||||
# ingress:
|
|
||||||
# enabled: true
|
|
||||||
# hosts:
|
|
||||||
# - "ldap.haumdaucher.de"
|
|
||||||
# annotations:
|
|
||||||
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
|
||||||
# tls:
|
|
||||||
# - ldap-haumdaucher-de
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: authentik-forwardauth
|
||||||
|
namespace: auth
|
||||||
|
spec:
|
||||||
|
forwardAuth:
|
||||||
|
address: http://authentik-server.auth.svc.cluster.local/outpost.goauthentik.io/auth/traefik
|
||||||
|
trustForwardHeader: true
|
||||||
|
authRequestHeaders:
|
||||||
|
- Authorization
|
||||||
|
- Cookie
|
||||||
|
- Accept
|
||||||
|
authResponseHeaders:
|
||||||
|
- X-authentik-username
|
||||||
|
- X-authentik-groups
|
||||||
|
- X-authentik-email
|
||||||
|
- X-authentik-name
|
||||||
|
- X-authentik-uid
|
||||||
|
- X-authentik-jwt
|
||||||
|
- X-authentik-meta-jwks
|
||||||
|
- X-authentik-meta-outpost
|
||||||
|
- X-authentik-meta-provider
|
||||||
|
- X-authentik-meta-app
|
||||||
|
- X-authentik-meta-version
|
||||||
|
- authorization
|
||||||
Binary file not shown.
@@ -1,13 +1,74 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: gitea-db-adminer
|
||||||
|
namespace: development
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: gitea-db-adminer
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: gitea-db-adminer
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: adminer
|
||||||
|
image: adminer:latest
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
name: http
|
||||||
|
env:
|
||||||
|
- name: ADMINER_DEFAULT_SERVER
|
||||||
|
value: "gitea-postgresql"
|
||||||
|
- name: ADMINER_DESIGN
|
||||||
|
value: "dracula"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 10m
|
||||||
|
memory: 32Mi
|
||||||
|
limits:
|
||||||
|
memory: 128Mi
|
||||||
---
|
---
|
||||||
db:
|
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
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: grabengasse-backend
|
||||||
|
namespace: grabengasse
|
||||||
|
labels:
|
||||||
|
app: grabengasse-backend
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: grabengasse-backend
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: grabengasse-backend
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: registry-haumdaucher-de
|
||||||
|
containers:
|
||||||
|
- name: backend
|
||||||
|
image: registry.haumdaucher.de/website/grabengasse_backend:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 8000
|
||||||
|
name: http
|
||||||
|
env:
|
||||||
|
- name: PROMETHEUS_URL
|
||||||
|
value: "http://prometheus-operator-kube-p-prometheus.monitoring.svc.cluster.local:9090"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 200m
|
||||||
|
memory: 256Mi
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 8000
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 8000
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 15
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: grabengasse-backend
|
||||||
|
namespace: grabengasse
|
||||||
|
labels:
|
||||||
|
app: grabengasse-backend
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 8000
|
||||||
|
targetPort: 8000
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: grabengasse-backend
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: grabengasse-frontend
|
||||||
|
namespace: grabengasse
|
||||||
|
labels:
|
||||||
|
app: grabengasse-frontend
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: grabengasse-frontend
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: grabengasse-frontend
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: registry-haumdaucher-de
|
||||||
|
containers:
|
||||||
|
- name: frontend
|
||||||
|
image: registry.haumdaucher.de/website/grabengasse_frontend:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
name: http
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 25m
|
||||||
|
memory: 32Mi
|
||||||
|
limits:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 80
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 80
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 15
|
||||||
|
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: grabengasse-frontend
|
||||||
|
namespace: grabengasse
|
||||||
|
labels:
|
||||||
|
app: grabengasse-frontend
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 80
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: grabengasse-frontend
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: grabengasse7
|
||||||
|
namespace: grabengasse
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: "traefik"
|
||||||
|
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||||
|
kubernetes.io/tls-acme: "true"
|
||||||
|
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||||
|
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- grabengasse7.de
|
||||||
|
secretName: grabengasse7-de
|
||||||
|
rules:
|
||||||
|
- host: grabengasse7.de
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /api
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: grabengasse-backend
|
||||||
|
port:
|
||||||
|
number: 8000
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: grabengasse-frontend
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: grabengasse
|
||||||
|
labels:
|
||||||
|
name: grabengasse
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -15,7 +15,7 @@ data:
|
|||||||
"tags": ["home", "weather", "outdoor", "prometheus"],
|
"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"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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.
@@ -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."
|
||||||
|
|||||||
+1
-1
Submodule knowledge-wiki updated: 69c3e840fc...d2f0913955
Generated
+32
-32
@@ -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.
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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" {
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
+2945
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
@@ -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)
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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;"]
|
||||||
@@ -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
BIN
Binary file not shown.
|
After Width: | Height: | Size: 436 B |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1019 B |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
+37
@@ -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>
|
||||||
@@ -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
@@ -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;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
@@ -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
@@ -0,0 +1 @@
|
|||||||
|
../esbuild/bin/esbuild
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../he/bin/he
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../nanoid/bin/nanoid.cjs
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../@babel/parser/bin/babel-parser.js
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../rollup/dist/bin/rollup
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../typescript/bin/tsc
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../typescript/bin/tsserver
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../vite/bin/vite.js
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
../vue-tsc/bin/vue-tsc.js
|
||||||
+794
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+118
@@ -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;
|
||||||
|
}
|
||||||
+22
@@ -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
@@ -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
|
||||||
|
```
|
||||||
Generated
Vendored
+295
@@ -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
|
||||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+31
@@ -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"
|
||||||
|
}
|
||||||
Generated
Vendored
+22
@@ -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.
|
||||||
Generated
Vendored
+19
@@ -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
|
||||||
|
```
|
||||||
Generated
Vendored
+70
@@ -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
|
||||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+57
@@ -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
|
||||||
Generated
Vendored
+1
@@ -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":[]}
|
||||||
Generated
Vendored
+35
@@ -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
|
||||||
Generated
Vendored
+1
@@ -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":[]}
|
||||||
Generated
Vendored
+31
@@ -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"
|
||||||
|
}
|
||||||
+1073
File diff suppressed because it is too large
Load Diff
+19
@@ -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
@@ -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
|
||||||
|
```
|
||||||
Generated
Vendored
Executable
+15
@@ -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, " "));
|
||||||
|
}
|
||||||
+14611
File diff suppressed because it is too large
Load Diff
+1
File diff suppressed because one or more lines are too long
+50
@@ -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"
|
||||||
|
}
|
||||||
Generated
Vendored
+262
@@ -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
@@ -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
@@ -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
|
||||||
|
```
|
||||||
+16
@@ -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
|
||||||
Generated
Vendored
+1
@@ -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":[]}
|
||||||
Generated
Vendored
+1251
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+3
@@ -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
Reference in New Issue
Block a user