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