diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..6217a27
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,8 @@
+Dockerfile
+.dockerignore
+node_modules
+npm-debug.log
+README.md
+.next
+.git
+.env
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..01473fa
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+dist
+.env
+*.local
diff --git a/README.md b/README.md
index 858278d..f465737 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,78 @@
-AZion
+# AZion Scheduler β High-Performance Process Dashboard
+
+AZion is a specialized Vite + React SPA designed for high-performance visualization and management of server-side processes, using **Baserow** as a headless backend.
+
+## π Quick Start
+
+1. **Install dependencies**: `npm install`
+2. **Setup environment**: Rename `.env.example` to `.env` and provide your Baserow API details.
+3. **Run Dev server**: `npm run dev`
+
+---
+
+## π Baserow & n8n Integration Guide
+
+This section outlines the exact internal logic the AZion dashboard uses to parse scheduling parameters. When inserting or updating records via an automation runtime like **n8n**, your JSON payload sent to the Baserow API must structurally respect these dependencies.
+
+### 1. Global / Universal Fields
+These fields are required or heavily impact the UI regardless of the execution type.
+
+- **`Name`** *(Single-line text)*: The display name of the process.
+- **`Server`** *(Linked row)*: Reference ID linking back to the Server Table. n8n payload syntax: `[123]` (Array of IDs).
+- **`Sortierung`** *(Number)*: The rendering order integer. `1` is pushed to the top of the timeline track.
+- **`Dauer`** *(Single-line text or Number)*: **Always specified in SECONDS**. Defines the length of the visual block. *(e.g., passing `"1800"` renders a 30-minute block).*
+- **`Farbe`** *(Single-line text)*: The hex code for the visual block. *(e.g., `"#3b82f6"`).*
+- **`Status`** *(Single select)*: E.g., `aktiv`, `inaktiv`, `fehler`.
+- **`Sichtbarkeit`** *(Single select)*: Declares semantic zoom constraints (`Immer`, `Max 6 Stunden`, `Max 1 Tag`, `Max 1 Woche`, `Max 1 Monat`).
+
+### 2. Dynamic Scheduling via `Wiederholung`
+The logic engine evaluates time solely based on what string is stored inside the **`Wiederholung`** *(Single select)* column.
+
+#### Pattern A: Standard Execution
+**Applies to**: `TAEGLICH`, `WOECHENTLICH`, `WERKTAGE`, `WOCHENENDE`, `MONATLICH_FESTER_TAG`
+These patterns expect a singular, definitive time of day.
+
+* **n8n Setup**:
+ * `Wiederholung`: `"TAEGLICH"`
+ * `Start`: `"HH:MM:SS"` *(e.g., `"08:30:00"`).*
+ * `Dauer`: `"60"` *(60 seconds).*
+
+#### Pattern B: Hourly Execution (`STUENDLICH`)
+* **n8n Setup**:
+ * `Wiederholung`: `"STUENDLICH"`
+ * `Start Minute`: `45` *(Integer 0β59).*
+ * `Dauer`: `"120"` *(2 minutes).*
+
+#### Pattern C: Custom Interval (`INTERVALL`)
+Recurrent blocks bounded between two timestamps.
+
+* **n8n Setup**:
+ * `Wiederholung`: `"INTERVALL"`
+ * `Erste AusfΓΌhrung`: `"HH:MM:SS"` *(Start bounds).*
+ * `AusfΓΌhrung bis`: `"HH:MM:SS"` *(End bounds).*
+ * `Intervall zwischen`: `"3600"` *(Spacing in SECONDS).*
+ * `Dauer`: `"300"` *(Duration in SECONDS).*
+
+---
+
+### 3. Example JSON Payload (n8n / API)
+
+```json
+{
+ "Name": "API Heartbeat Checker",
+ "Server": [45],
+ "Wiederholung": "INTERVALL",
+ "Erste AusfΓΌhrung": "08:00:00",
+ "AusfΓΌhrung bis": "17:00:00",
+ "Intervall zwischen": "300",
+ "Dauer": "15",
+ "Status": "aktiv",
+ "Farbe": "#22c55e",
+ "Sortierung": 8
+}
+```
+
+### π§ Performance & Logic Notes
+- **Block Merging**: The Gantt Chart automatically merges overlapping blocks from the same process to prevent DOM flooding when zoomed out.
+- **Max View Filter**: High-frequency tasks (like 1-min intervals) are hidden in Monthly/Weekly views unless `Sichtbarkeit` is set to "Immer" to maintain UI responsiveness.
+- **Auto-Sort**: The frontend automatically finds the next highest `Sortierung` index when creating new tasks via the GUI.
diff --git a/check-ui.js b/check-ui.js
new file mode 100644
index 0000000..15a4ce1
--- /dev/null
+++ b/check-ui.js
@@ -0,0 +1,26 @@
+const puppeteer = require('puppeteer');
+(async () => {
+ const browser = await puppeteer.launch();
+ const page = await browser.newPage();
+
+ page.on('pageerror', err => console.log('Runtime Error:', err.message));
+ page.on('console', msg => console.log('Console:', msg.text()));
+
+ await page.goto('http://localhost:3001');
+ await page.waitForTimeout(2000);
+
+ // Evaluate if Gantt DOM structural integrity exists
+ const ganttMetrics = await page.evaluate(() => {
+ const blocks = document.querySelectorAll('.gantt-block');
+ const rows = document.querySelectorAll('.gantt-row');
+ return {
+ blockCount: blocks.length,
+ rowCount: rows.length,
+ blocksWidth: Array.from(blocks).slice(0,5).map(b => b.style.width),
+ blocksLeft: Array.from(blocks).slice(0,5).map(b => b.style.left)
+ };
+ });
+ console.log("Metrics:", JSON.stringify(ganttMetrics, null, 2));
+
+ await browser.close();
+})();
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..9aede9f
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ AZion Scheduler β AZ INTEC GmbH
+
+
+
+
+
+
diff --git a/logo.png b/logo.png
new file mode 100644
index 0000000..59f0bea
Binary files /dev/null and b/logo.png differ
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..7d58cae
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1771 @@
+{
+ "name": "azion-scheduler",
+ "version": "2.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "azion-scheduler",
+ "version": "2.0.0",
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.23.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^4.3.1",
+ "typescript": "^5.5.3",
+ "vite": "^5.3.4"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "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/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.23.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
+ "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
+ "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz",
+ "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz",
+ "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz",
+ "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz",
+ "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz",
+ "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz",
+ "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz",
+ "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz",
+ "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz",
+ "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz",
+ "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz",
+ "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz",
+ "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz",
+ "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz",
+ "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz",
+ "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",
+ "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",
+ "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
+ "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",
+ "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",
+ "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",
+ "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",
+ "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz",
+ "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz",
+ "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.28",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
+ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.23",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.23.tgz",
+ "integrity": "sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001791",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz",
+ "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.344",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz",
+ "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "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/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "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/node-releases": {
+ "version": "2.0.38",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
+ "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
+ "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==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.12",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
+ "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
+ "dev": true,
+ "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.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
+ "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
+ "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2",
+ "react-router": "6.30.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
+ "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.2",
+ "@rollup/rollup-android-arm64": "4.60.2",
+ "@rollup/rollup-darwin-arm64": "4.60.2",
+ "@rollup/rollup-darwin-x64": "4.60.2",
+ "@rollup/rollup-freebsd-arm64": "4.60.2",
+ "@rollup/rollup-freebsd-x64": "4.60.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.2",
+ "@rollup/rollup-linux-arm64-musl": "4.60.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.2",
+ "@rollup/rollup-linux-loong64-musl": "4.60.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-musl": "4.60.2",
+ "@rollup/rollup-openbsd-x64": "4.60.2",
+ "@rollup/rollup-openharmony-arm64": "4.60.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.2",
+ "@rollup/rollup-win32-x64-gnu": "4.60.2",
+ "@rollup/rollup-win32-x64-msvc": "4.60.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "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/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..668b80a
--- /dev/null
+++ b/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "azion-scheduler",
+ "private": true,
+ "version": "2.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.23.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^4.3.1",
+ "typescript": "^5.5.3",
+ "vite": "^5.3.4"
+ }
+}
diff --git a/public/.gitkeep b/public/.gitkeep
new file mode 100644
index 0000000..490f444
--- /dev/null
+++ b/public/.gitkeep
@@ -0,0 +1 @@
+# Empty Gitkeep File
diff --git a/public/favicon.png b/public/favicon.png
new file mode 100644
index 0000000..984cfb6
Binary files /dev/null and b/public/favicon.png differ
diff --git a/public/logo.png b/public/logo.png
new file mode 100644
index 0000000..59f0bea
Binary files /dev/null and b/public/logo.png differ
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..1ea9acd
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,37 @@
+import { useState, useEffect } from 'react'
+import { Routes, Route } from 'react-router-dom'
+import Sidebar from './components/Sidebar'
+import Timeline from './pages/Timeline'
+import Servers from './pages/Servers'
+import Processes from './pages/Processes'
+
+export default function App() {
+ const [now, setNow] = useState(new Date())
+
+ useEffect(() => {
+ const timer = setInterval(() => setNow(new Date()), 1000)
+ return () => clearInterval(timer)
+ }, [])
+
+ return (
+
+
+
+
+
+
+ {now.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })} {now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
+
+
AZ INTEC GmbH
+
+
+
+ } />
+ } />
+ } />
+
+
+
+
+ )
+}
diff --git a/src/api/baserow.ts b/src/api/baserow.ts
new file mode 100644
index 0000000..de4eb71
--- /dev/null
+++ b/src/api/baserow.ts
@@ -0,0 +1,104 @@
+const BASE = import.meta.env.VITE_BASEROW_URL;
+const TOKEN = import.meta.env.VITE_BASEROW_TOKEN;
+const SERVERS_TABLE = import.meta.env.VITE_SERVERS_TABLE_ID;
+const PROCESSES_TABLE = import.meta.env.VITE_PROCESSES_TABLE_ID;
+
+const headers = {
+ Authorization: `Token ${TOKEN}`,
+ 'Content-Type': 'application/json',
+};
+
+/* ββ Types βββββββββββββββββββββββββββββββββββββββββββ */
+
+export interface Server {
+ id: number;
+ Name: string;
+ Beschreibung: string;
+ Sortierung?: number;
+ Prozesse: { id: number; value: string }[];
+}
+
+export interface Process {
+ id: number;
+ Name: string;
+ Server?: { id: number; value: string }[];
+ Status?: { id: number; value: string; color: string };
+ Wiederholung?: { id: number; value: string; color: string };
+ Farbe?: string;
+ Start?: string;
+ Ende?: string;
+ 'Start Minute'?: number | null;
+ Dauer?: string;
+ 'Erste AusfΓΌhrung'?: string;
+ 'AusfΓΌhrung bis'?: string;
+ 'Intervall zwischen'?: string;
+ Wochentage?: string;
+ Sichtbarkeit?: { id: number; value: string; color: string };
+ Sortierung?: number;
+}
+
+interface BaserowList {
+ count: number;
+ next: string | null;
+ previous: string | null;
+ results: T[];
+}
+
+/* ββ Generic helpers βββββββββββββββββββββββββββββββββ */
+
+async function listRows(tableId: string): Promise {
+ const res = await fetch(
+ `${BASE}/api/database/rows/table/${tableId}/?user_field_names=true&size=200`,
+ { headers }
+ );
+ if (!res.ok) throw new Error(`Baserow GET failed: ${res.status}`);
+ const data: BaserowList = await res.json();
+ return data.results;
+}
+
+async function createRow(tableId: string, fields: Record) {
+ const res = await fetch(
+ `${BASE}/api/database/rows/table/${tableId}/?user_field_names=true`,
+ { method: 'POST', headers, body: JSON.stringify(fields) }
+ );
+ if (!res.ok) throw new Error(`Baserow POST failed: ${res.status}`);
+ return res.json();
+}
+
+async function updateRow(tableId: string, rowId: number, fields: Record) {
+ const res = await fetch(
+ `${BASE}/api/database/rows/table/${tableId}/${rowId}/?user_field_names=true`,
+ { method: 'PATCH', headers, body: JSON.stringify(fields) }
+ );
+ if (!res.ok) throw new Error(`Baserow PATCH failed: ${res.status}`);
+ return res.json();
+}
+
+async function deleteRow(tableId: string, rowId: number) {
+ const res = await fetch(
+ `${BASE}/api/database/rows/table/${tableId}/${rowId}/`,
+ { method: 'DELETE', headers }
+ );
+ if (!res.ok) throw new Error(`Baserow DELETE failed: ${res.status}`);
+}
+
+/* ββ Public API ββββββββββββββββββββββββββββββββββββββ */
+
+export const api = {
+ // Servers
+ getServers: () => listRows(SERVERS_TABLE),
+ createServer: (name: string, description: string) =>
+ createRow(SERVERS_TABLE, { Name: name, Beschreibung: description }),
+ deleteServer: (id: number) => deleteRow(SERVERS_TABLE, id),
+
+ // Processes
+ getProcesses: () => listRows(PROCESSES_TABLE),
+ createProcess: (fields: Record) =>
+ createRow(PROCESSES_TABLE, fields),
+ updateProcess: (id: number, fields: Record) => {
+ // If fields.Sichtbarkeit is undefined, it won't be sent, which protects against the field missing.
+ const cleanFields = Object.fromEntries(Object.entries(fields).filter(([_, v]) => v !== undefined));
+ return updateRow(PROCESSES_TABLE, id, cleanFields);
+ },
+ deleteProcess: (id: number) => deleteRow(PROCESSES_TABLE, id),
+};
diff --git a/src/components/GanttChart.tsx b/src/components/GanttChart.tsx
new file mode 100644
index 0000000..f7f0862
--- /dev/null
+++ b/src/components/GanttChart.tsx
@@ -0,0 +1,457 @@
+import React, { useState, useEffect } from 'react'
+import type { Process, Server } from '../api/baserow'
+
+const SERVER_ICON = (
+
+)
+
+type ViewMode = '2H' | '6H' | 'Arbeitszeit' | 'Nachts' | 'Tag' | 'Woche' | 'Monat'
+
+function getViewBounds(mode: ViewMode, referenceDate: Date): { start: Date, end: Date, spanHours: number } {
+ const d = new Date(referenceDate)
+ let start = new Date(d)
+ let end = new Date(d)
+ let spanHours = 0
+
+ if (mode === '2H') {
+ start = new Date(d.getTime() - 1 * 3600_000)
+ end = new Date(d.getTime() + 1 * 3600_000)
+ spanHours = 2
+ } else if (mode === '6H') {
+ start = new Date(d.getTime() - 3 * 3600_000)
+ end = new Date(d.getTime() + 3 * 3600_000)
+ spanHours = 6
+ } else if (mode === 'Arbeitszeit') {
+ start.setHours(6, 0, 0, 0)
+ end.setHours(18, 0, 0, 0)
+ spanHours = 12
+ } else if (mode === 'Nachts') {
+ if (d.getHours() < 6) d.setDate(d.getDate() - 1)
+ start = new Date(d)
+ start.setHours(18, 0, 0, 0)
+ end = new Date(start.getTime() + 12 * 3600_000)
+ spanHours = 12
+ } else if (mode === 'Tag') {
+ start.setHours(0, 0, 0, 0)
+ end = new Date(start.getTime() + 24 * 3600_000)
+ spanHours = 24
+ } else if (mode === 'Woche') {
+ const day = start.getDay()
+ const diff = start.getDate() - day + (day === 0 ? -6 : 1)
+ start.setDate(diff)
+ start.setHours(0, 0, 0, 0)
+ end = new Date(start.getTime() + 7 * 24 * 3600_000)
+ spanHours = 168
+ } else if (mode === 'Monat') {
+ start.setDate(1)
+ start.setHours(0, 0, 0, 0)
+ end = new Date(start.getTime())
+ end.setMonth(end.getMonth() + 1)
+ spanHours = (end.getTime() - start.getTime()) / 3600_000
+ }
+
+ return { start, end, spanHours }
+}
+
+function parseTime(tStr: string | null | undefined): [number, number, number] {
+ if (!tStr) return [12, 0, 0]
+ const [h, m, s] = tStr.split(':').map(Number)
+ return [isNaN(h) ? 12 : h, isNaN(m) ? 0 : m, isNaN(s) ? 0 : s]
+}
+
+export default function GanttChart({ processes, servers, onRunningChange }: { processes: Process[], servers?: Server[], onRunningChange?: (count: number) => void }) {
+ const [view, setView] = useState('Tag')
+ const [now, setNow] = useState(new Date())
+
+ useEffect(() => {
+ const interval = setInterval(() => setNow(new Date()), 60_000)
+ return () => clearInterval(interval)
+ }, [])
+
+ // Calculate Running Processes whenever now or processes change
+ useEffect(() => {
+ if (!onRunningChange) return
+
+ let count = 0
+ const nowMs = now.getTime()
+
+ for (const proc of processes) {
+ if (proc.Status?.value !== 'aktiv') continue
+
+ // Minimal version of renderBlocks logic to find if any block overlaps 'now'
+ const rep = proc.Wiederholung?.value
+ let isRunning = false
+
+ // We only need to check today's blocks for 'now'
+ const iter = new Date(now)
+ iter.setHours(0, 0, 0, 0)
+
+ const check = (stMs: number, durSecs: number) => {
+ const edMs = stMs + (durSecs * 1000)
+ return nowMs >= stMs && nowMs <= edMs
+ }
+
+ const dur = parseInt(proc.Dauer || '0') || 60
+ const dayStrings = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']
+ const currentDayStr = dayStrings[iter.getDay()]
+
+ // Weekday filter
+ if (proc.Wochentage && proc.Wochentage.trim() !== '') {
+ if (!proc.Wochentage.includes(currentDayStr) && rep !== 'WOCHENENDE' && rep !== 'WERKTAGE') {
+ continue
+ }
+ }
+
+ if (rep === 'TAEGLICH') {
+ const [h, m, s] = parseTime(proc.Start)
+ const d = new Date(iter)
+ d.setHours(h, m, s)
+ if (check(d.getTime(), dur)) isRunning = true
+ } else if (rep === 'STUENDLICH') {
+ const d = new Date(iter)
+ d.setHours(now.getHours(), proc['Start Minute'] || 0, 0)
+ if (check(d.getTime(), dur)) isRunning = true
+ } else if (rep === 'WOECHENTLICH') {
+ const [h, m, s] = parseTime(proc.Start)
+ const d = new Date(iter)
+ d.setHours(h, m, s)
+ if (check(d.getTime(), dur)) isRunning = true
+ } else if (rep === 'WERKTAGE') {
+ const day = iter.getDay()
+ if (day >= 1 && day <= 5) {
+ const [h, m, s] = parseTime(proc.Start)
+ const d = new Date(iter)
+ d.setHours(h, m, s)
+ if (check(d.getTime(), dur)) isRunning = true
+ }
+ } else if (rep === 'WOCHENENDE') {
+ const day = iter.getDay()
+ if (day === 0 || day === 6) {
+ const [h, m, s] = parseTime(proc.Start)
+ const d = new Date(iter)
+ d.setHours(h, m, s)
+ if (check(d.getTime(), dur)) isRunning = true
+ }
+ } else if (rep === 'INTERVALL') {
+ const [fh, fm, fs] = parseTime(proc['Erste AusfΓΌhrung'])
+ const [wh, wm, ws] = parseTime(proc['AusfΓΌhrung bis'] || '23:59:59')
+ const interval = Math.max(1, parseInt(proc['Intervall zwischen'] || '0') || 60)
+
+ const dayStart = new Date(iter)
+ dayStart.setHours(fh, fm, fs)
+ const dayEnd = new Date(iter)
+ dayEnd.setHours(wh, wm, ws)
+
+ // Find if any interval in this day contains 'now'
+ if (nowMs >= dayStart.getTime() && nowMs <= dayEnd.getTime()) {
+ const diffSeconds = (nowMs - dayStart.getTime()) / 1000
+ const remainder = diffSeconds % interval
+ if (remainder <= dur) isRunning = true
+ }
+ } else if (rep === 'MONATLICH_FESTER_TAG') {
+ if (iter.getDate() === 1) {
+ const [h, m, s] = parseTime(proc.Start)
+ const d = new Date(iter)
+ d.setHours(h, m, s)
+ if (check(d.getTime(), dur)) isRunning = true
+ }
+ }
+
+ if (isRunning) count++
+ }
+
+ onRunningChange(count)
+ }, [now, processes, onRunningChange])
+
+ const { start: viewStart, end: viewEnd, spanHours } = getViewBounds(view, now)
+ const spanMs = viewEnd.getTime() - viewStart.getTime()
+
+ // Generate Hour Markers
+ const markers = []
+ let mkIter = new Date(viewStart)
+
+ // Dynamic Snapping
+ if (view === '2H') {
+ mkIter.setMinutes(Math.floor(mkIter.getMinutes() / 15) * 15, 0, 0)
+ } else if (view === '6H') {
+ mkIter.setMinutes(Math.floor(mkIter.getMinutes() / 30) * 30, 0, 0)
+ } else if (view === 'Monat') {
+ mkIter.setHours(0, 0, 0, 0)
+ } else if (view === 'Woche') {
+ mkIter.setHours(Math.floor(mkIter.getHours() / 6) * 6, 0, 0, 0)
+ } else {
+ mkIter.setMinutes(0, 0, 0)
+ }
+
+ while (mkIter <= viewEnd) {
+ const pct = ((mkIter.getTime() - viewStart.getTime()) / spanMs) * 100
+ let label = ''
+
+ if (view === 'Monat') {
+ label = mkIter.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' })
+ } else if (view === 'Woche') {
+ if (mkIter.getHours() === 0) {
+ label = mkIter.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: '2-digit' })
+ } else {
+ label = mkIter.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })
+ }
+ } else {
+ label = mkIter.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })
+ }
+
+ if (pct >= 0 && pct <= 100) {
+ markers.push({ key: mkIter.getTime(), pct, label })
+ }
+
+ // Dynamic Increment
+ if (view === '2H') mkIter.setMinutes(mkIter.getMinutes() + 15)
+ else if (view === '6H') mkIter.setMinutes(mkIter.getMinutes() + 30)
+ else if (view === 'Woche') mkIter.setHours(mkIter.getHours() + 6)
+ else if (view === 'Monat') mkIter.setDate(mkIter.getDate() + 1)
+ else mkIter.setHours(mkIter.getHours() + 1)
+ }
+
+ // Now line
+ const nowPct = ((now.getTime() - viewStart.getTime()) / spanMs) * 100
+ const isNowVisible = nowPct >= 0 && nowPct <= 100
+
+ // Group processes by server
+ const serverGroups = new Map()
+ for (const p of processes) {
+ // Only render active processes
+ if (p.Status?.value && p.Status.value !== 'aktiv') continue
+
+ // APPLY VISIBILITY MAX VIEW FILTER (CRITICAL MEMORY PRESERVATION)
+ const maxView = p.Sichtbarkeit?.value || 'Immer'
+ if (maxView === 'Max 6 Stunden') {
+ if (view === 'Tag' || view === 'Woche' || view === 'Monat' || view === 'Arbeitszeit' || view === 'Nachts') continue
+ } else if (maxView === 'Max 1 Tag') {
+ if (view === 'Woche' || view === 'Monat') continue
+ }
+
+ const sName = p.Server?.[0]?.value || 'Ohne Server'
+ if (!serverGroups.has(sName)) serverGroups.set(sName, [])
+ serverGroups.get(sName)!.push(p)
+ }
+
+ const renderBlocksForProcess = (proc: Process) => {
+ const rawBlocks: { stMs: number; edMs: number }[] = []
+ const rep = proc.Wiederholung?.value
+
+ let iter = new Date(viewStart)
+ iter.setHours(0, 0, 0, 0)
+ // Add one day padding
+ iter.setDate(iter.getDate() - 1)
+ const iterEnd = new Date(viewEnd)
+ iterEnd.setDate(iterEnd.getDate() + 1)
+
+ const pushRawBlock = (startTimeStr: Date, durationSecs: number) => {
+ const stMs = startTimeStr.getTime()
+ const edMs = stMs + (durationSecs * 1000)
+ if (edMs < viewStart.getTime() || stMs > viewEnd.getTime()) return
+ rawBlocks.push({ stMs, edMs })
+ }
+
+ while (iter < iterEnd) {
+ // Apply weekday filter globally
+ const dayStrings = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']
+ const currentDayStr = dayStrings[iter.getDay()]
+ if (proc.Wochentage && proc.Wochentage.trim() !== '') {
+ if (!proc.Wochentage.includes(currentDayStr) && rep !== 'WOCHENENDE' && rep !== 'WERKTAGE') {
+ iter.setDate(iter.getDate() + 1)
+ continue
+ }
+ }
+
+ if (rep === 'TAEGLICH') {
+ const [h, m, s] = parseTime(proc.Start)
+ const d = new Date(iter)
+ d.setHours(h, m, s)
+ pushRawBlock(d, parseInt(proc.Dauer || '0') || 60)
+ } else if (rep === 'STUENDLICH') {
+ for (let hh = 0; hh < 24; hh++) {
+ const d = new Date(iter)
+ d.setHours(hh, proc['Start Minute'] || 0, 0)
+ pushRawBlock(d, Math.max(1, parseInt(proc.Dauer || '0') || 60))
+ }
+ } else if (rep === 'WOECHENTLICH') {
+ const [h, m, s] = parseTime(proc.Start)
+ const d = new Date(iter)
+ d.setHours(h, m, s)
+ pushRawBlock(d, parseInt(proc.Dauer || '0') || 60)
+ } else if (rep === 'WERKTAGE') {
+ const day = iter.getDay()
+ if (day >= 1 && day <= 5) {
+ const [h, m, s] = parseTime(proc.Start)
+ const d = new Date(iter)
+ d.setHours(h, m, s)
+ pushRawBlock(d, parseInt(proc.Dauer || '0') || 60)
+ }
+ } else if (rep === 'WOCHENENDE') {
+ const day = iter.getDay()
+ if (day === 0 || day === 6) {
+ const [h, m, s] = parseTime(proc.Start)
+ const d = new Date(iter)
+ d.setHours(h, m, s)
+ pushRawBlock(d, parseInt(proc.Dauer || '0') || 60)
+ }
+ } else if (rep === 'INTERVALL') {
+ const [fh, fm, fs] = parseTime(proc['Erste AusfΓΌhrung'])
+ const [wh, wm, ws] = parseTime(proc['AusfΓΌhrung bis'] || '23:59:59')
+ const interval = Math.max(1, parseInt(proc['Intervall zwischen'] || '0') || 60)
+ const dur = parseInt(proc.Dauer || '0') || 60
+
+ const dayStart = new Date(iter)
+ dayStart.setHours(fh, fm, fs)
+ const dayEnd = new Date(iter)
+ dayEnd.setHours(wh, wm, ws)
+
+ for (let t = dayStart.getTime(); t <= dayEnd.getTime(); t += interval * 1000) {
+ pushRawBlock(new Date(t), dur)
+ }
+ } else if (rep === 'MONATLICH_FESTER_TAG') {
+ if (iter.getDate() === 1) {
+ const [h, m, s] = parseTime(proc.Start)
+ const d = new Date(iter)
+ d.setHours(h, m, s)
+ pushRawBlock(d, parseInt(proc.Dauer || '0') || 60)
+ }
+ } else {
+ const [h, m, s] = parseTime(proc.Start || '12:00:00')
+ const d = new Date(iter)
+ d.setHours(h, m, s)
+ pushRawBlock(d, parseInt(proc.Dauer || '0') || 30)
+ }
+ iter.setDate(iter.getDate() + 1)
+ }
+
+ // Sort blocks by start time
+ rawBlocks.sort((a, b) => a.stMs - b.stMs)
+
+ // Merge blocks that are overlapping or too close to render individually (e.g. gap < 0.2% of screen)
+ const mergedBlocks: { stMs: number; edMs: number }[] = []
+ // 0.2% of total span ms is threshold for merging
+ const mergeThreshold = spanMs * 0.002
+
+ for (const b of rawBlocks) {
+ if (mergedBlocks.length === 0) {
+ mergedBlocks.push({ ...b })
+ } else {
+ const prev = mergedBlocks[mergedBlocks.length - 1]
+ // If start is before or overlaps with prev.end + visual threshold
+ if (b.stMs <= prev.edMs + mergeThreshold) {
+ prev.edMs = Math.max(prev.edMs, b.edMs)
+ } else {
+ mergedBlocks.push({ ...b })
+ }
+ }
+ }
+
+ // Now map them to standard JSX layout
+ return mergedBlocks.map(b => {
+ const left = Math.max(0, ((b.stMs - viewStart.getTime()) / spanMs) * 100)
+ const rightBounds = Math.min(viewEnd.getTime(), b.edMs)
+ let width = ((rightBounds - Math.max(viewStart.getTime(), b.stMs)) / spanMs) * 100
+
+ if (width < 0.2) width = 0.2
+
+ return (
+
+ {width > 5 && {proc.Name}}
+
+ )
+ })
+ }
+
+ return (
+
+
+ {/* Zoom controls */}
+
+
Timeline Setup
+
+ {(['2H', '6H', 'Arbeitszeit', 'Nachts', 'Tag', 'Woche', 'Monat'] as ViewMode[]).map(v => (
+
+ ))}
+
+
+
+
+ {/* Header markers */}
+
+ {markers.map(m => (
+
+ {m.label}
+
+ ))}
+
+
+ {/* Chart body */}
+
+ {isNowVisible && (
+
+ )}
+
+ {Array.from(serverGroups.entries())
+ .sort(([sNameA], [sNameB]) => {
+ if (!servers) return 0
+ const sA = servers.find(s => s.Name === sNameA)
+ const sB = servers.find(s => s.Name === sNameB)
+ return (sA?.Sortierung ?? 9999) - (sB?.Sortierung ?? 9999)
+ })
+ .map(([serverName, procs]) => {
+ const sortedProcs = [...procs].sort((a, b) => (a.Sortierung ?? 9999) - (b.Sortierung ?? 9999))
+
+ return (
+
+
+
+ {SERVER_ICON}
+ {serverName}
+
+ {sortedProcs.length > 0 && (
+
+ {sortedProcs.map(p => (
+
+
+ {p.Name}
+
+ ))}
+
+ )}
+
+
+ {sortedProcs.map(p => (
+
+ {renderBlocksForProcess(p)}
+
+ ))}
+
+
+ )})}
+
+ {serverGroups.size === 0 && (
+
+ Keine aktiven Prozesse in dieser Ansicht sichtbar.
+
+ )}
+
+
+
+ )
+}
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
new file mode 100644
index 0000000..e146a4f
--- /dev/null
+++ b/src/components/Sidebar.tsx
@@ -0,0 +1,76 @@
+import { useLocation, Link } from 'react-router-dom'
+import { useState, useEffect } from 'react'
+
+const navItems = [
+ {
+ href: '/',
+ label: 'Timeline',
+ icon: (
+
+ ),
+ },
+ {
+ href: '/servers',
+ label: 'Server',
+ icon: (
+
+ ),
+ },
+ {
+ href: '/processes',
+ label: 'Prozesse',
+ icon: (
+
+ ),
+ },
+]
+
+export default function Sidebar() {
+ const { pathname } = useLocation()
+ const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'dark')
+
+ useEffect(() => {
+ document.documentElement.setAttribute('data-theme', theme)
+ localStorage.setItem('theme', theme)
+ }, [theme])
+
+ const toggleTheme = () => setTheme(t => (t === 'dark' ? 'light' : 'dark'))
+
+ return (
+
+ )
+}
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..072fb21
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,783 @@
+/* ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ AZion Scheduler β Design System
+ Dark + Light themes, vanilla CSS
+ ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+
+/* ββ Reset & Base ββββββββββββββββββββββββββββββββββββ */
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+:root {
+ --font: 'Corbel', 'Segoe UI', system-ui, -apple-system, sans-serif;
+ --radius: 8px;
+ --radius-lg: 12px;
+ --transition: 150ms ease;
+
+ /* ββ Dark theme (default) ββ */
+ --bg: hsl(220 16% 10%);
+ --bg-card: hsl(220 16% 13%);
+ --bg-sidebar: hsl(220 18% 8%);
+ --bg-topbar: hsl(220 18% 9%);
+ --bg-input: hsl(220 14% 16%);
+ --bg-hover: hsl(220 14% 18%);
+
+ --fg: hsl(210 20% 93%);
+ --fg-muted: hsl(215 16% 55%);
+ --fg-dim: hsl(215 10% 40%);
+
+ --border: hsl(220 14% 20%);
+ --border-light: hsl(220 14% 16%);
+
+ --primary: hsl(4 87% 47%);
+ --primary-fg: #fff;
+ --primary-dim: hsl(4 87% 47% / 0.15);
+
+ --green: hsl(142 71% 45%);
+ --yellow: hsl(38 92% 50%);
+ --blue: hsl(217 91% 60%);
+
+ color-scheme: dark;
+}
+
+/* ββ Light theme βββββββββββββββββββββββββββββββββββββ */
+[data-theme="light"] {
+ --bg: hsl(220 14% 96%);
+ --bg-card: hsl(0 0% 100%);
+ --bg-sidebar: hsl(0 0% 100%);
+ --bg-topbar: hsl(0 0% 99%);
+ --bg-input: hsl(220 14% 93%);
+ --bg-hover: hsl(220 10% 90%);
+
+ --fg: hsl(220 25% 12%);
+ --fg-muted: hsl(215 16% 47%);
+ --fg-dim: hsl(215 10% 65%);
+
+ --border: hsl(220 14% 85%);
+ --border-light: hsl(220 14% 90%);
+
+ color-scheme: light;
+}
+
+html {
+ font-family: var(--font);
+ font-size: 14px;
+}
+
+body {
+ background: var(--bg);
+ color: var(--fg);
+ min-height: 100vh;
+ transition: background var(--transition), color var(--transition);
+ -webkit-font-smoothing: antialiased;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+button {
+ font: inherit;
+ cursor: pointer;
+ border: none;
+ background: none;
+ color: inherit;
+}
+
+input,
+select,
+textarea {
+ font: inherit;
+ color: var(--fg);
+ background: var(--bg-input);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 8px 12px;
+ outline: none;
+ transition: border-color var(--transition);
+}
+
+input:focus,
+select:focus,
+textarea:focus {
+ border-color: var(--primary);
+}
+
+/* ββ Scrollbar βββββββββββββββββββββββββββββββββββββββ */
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--bg);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--border);
+ border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--primary);
+}
+
+/* ββ App Layout ββββββββββββββββββββββββββββββββββββββ */
+.app-layout {
+ display: flex;
+ min-height: 100vh;
+}
+
+/* ββ Sidebar βββββββββββββββββββββββββββββββββββββββββ */
+.sidebar {
+ width: 220px;
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ background: var(--bg-sidebar);
+ border-right: 1px solid var(--border);
+}
+
+.sidebar-logo {
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ gap: 0px;
+}
+
+.sidebar-logo a {
+ display: flex;
+ align-items: center;
+ gap: 0px;
+}
+
+.sidebar-logo img {
+ height: 28px;
+ width: auto;
+}
+
+.sidebar-logo .logo-suffix {
+ font-size: 42px;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+}
+
+.sidebar-logo .subtitle {
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.15em;
+ color: var(--fg-muted);
+ font-weight: 500;
+ padding-left: 2px;
+}
+
+.sidebar-nav {
+ flex: 1;
+ padding: 12px 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.sidebar-nav a {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 9px 12px;
+ border-radius: var(--radius);
+ font-size: 13px;
+ font-weight: 1000;
+ color: var(--fg-muted);
+ transition: all var(--transition);
+ border-left: 3px solid transparent;
+}
+
+.sidebar-nav a:hover {
+ background: var(--bg-hover);
+ color: var(--fg);
+}
+
+.sidebar-nav a.active {
+ background: var(--primary-dim);
+ color: var(--primary);
+ border-left-color: var(--primary);
+}
+
+.sidebar-nav a svg {
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+}
+
+.sidebar-footer {
+ padding: 12px 16px;
+ border-top: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.sidebar-footer .theme-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 11px;
+ color: var(--fg-muted);
+ font-weight: 500;
+}
+
+.sidebar-footer .version {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 10px;
+ color: var(--fg-dim);
+ font-family: monospace;
+}
+
+.sidebar-footer .version img {
+ height: 14px;
+ opacity: 0.6;
+}
+
+/* ββ Theme Toggle ββββββββββββββββββββββββββββββββββββ */
+.theme-toggle {
+ width: 40px;
+ height: 22px;
+ border-radius: 11px;
+ background: var(--border);
+ position: relative;
+ transition: background var(--transition);
+ cursor: pointer;
+}
+
+.theme-toggle::after {
+ content: '';
+ position: absolute;
+ top: 3px;
+ left: 3px;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ background: var(--primary);
+ transition: transform var(--transition);
+}
+
+[data-theme="light"] .theme-toggle::after {
+ transform: translateX(18px);
+}
+
+/* ββ Topbar ββββββββββββββββββββββββββββββββββββββββββ */
+.topbar {
+ position: sticky;
+ top: 0;
+ z-index: 40;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 24px;
+ border-bottom: 1px solid var(--border);
+ background: var(--bg-topbar);
+ backdrop-filter: blur(12px);
+}
+
+.topbar .status {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 16px;
+ color: var(--fg-muted);
+}
+
+.topbar .status-dot {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: var(--green);
+ box-shadow: 0 0 6px hsl(142 71% 45% / 0.7);
+ animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+
+ 50% {
+ opacity: 0.5;
+ }
+}
+
+.topbar .badge {
+ font-size: 12px;
+ font-family: monospace;
+ color: var(--fg-dim);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 3px 8px;
+}
+
+/* ββ Main Content ββββββββββββββββββββββββββββββββββββ */
+.main {
+ flex: 1;
+ overflow: auto;
+ min-height: 100vh;
+}
+
+.main-content {
+ padding: 24px;
+}
+
+/* ββ Page Header βββββββββββββββββββββββββββββββββββββ */
+.page-header {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ margin-bottom: 24px;
+}
+
+.page-header h1 {
+ font-size: 24px;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+}
+
+.page-header p {
+ font-size: 13px;
+ color: var(--fg-muted);
+ margin-top: 2px;
+}
+
+/* ββ Buttons βββββββββββββββββββββββββββββββββββββββββ */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 16px;
+ border-radius: var(--radius);
+ font-size: 16px;
+ font-weight: 600;
+ transition: all var(--transition);
+}
+
+.btn-primary {
+ background: var(--primary);
+ color: var(--primary-fg);
+ box-shadow: 0 1px 3px hsl(4 87% 47% / 0.3);
+}
+
+.btn-primary:hover {
+ filter: brightness(1.1);
+}
+
+.btn-ghost {
+ background: var(--primary-dim);
+ color: var(--primary);
+ border: 1px solid hsl(4 87% 47% / 0.3);
+}
+
+.btn-ghost:hover {
+ background: hsl(4 87% 47% / 0.25);
+}
+
+.btn-danger {
+ background: transparent;
+ color: hsl(0 84% 60%);
+ padding: 6px 10px;
+ font-size: 12px;
+ border-radius: var(--radius);
+}
+
+.btn-danger:hover {
+ background: hsl(0 84% 60% / 0.1);
+}
+
+.btn-sm {
+ padding: 5px 10px;
+ font-size: 12px;
+}
+
+/* ββ KPI Cards βββββββββββββββββββββββββββββββββββββββ */
+.kpi-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: 12px;
+ margin-bottom: 24px;
+}
+
+.kpi-card {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: 16px 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.kpi-card .kpi-label {
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--fg-muted);
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.kpi-card .kpi-label svg {
+ width: 14px;
+ height: 14px;
+}
+
+.kpi-card .kpi-value {
+ font-size: 28px;
+ font-weight: 700;
+}
+
+/* ββ Cards Grid ββββββββββββββββββββββββββββββββββββββ */
+.cards-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 16px;
+}
+
+.card {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: 20px;
+ transition: border-color var(--transition);
+}
+
+.card:hover {
+ border-color: hsl(4 87% 47% / 0.3);
+}
+
+.card h3 {
+ font-size: 16px;
+ font-weight: 700;
+ margin-bottom: 4px;
+}
+
+.card .desc {
+ font-size: 12px;
+ color: var(--fg-muted);
+ margin-bottom: 12px;
+}
+
+.card-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding-top: 12px;
+ border-top: 1px solid var(--border);
+ margin-top: 12px;
+}
+
+.card-footer .tag {
+ font-size: 12px;
+ background: var(--bg-hover);
+ padding: 3px 8px;
+ border-radius: var(--radius);
+}
+
+/* ββ Table βββββββββββββββββββββββββββββββββββββββββββ */
+.data-table {
+ width: 100%;
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ border-collapse: collapse;
+}
+
+.data-table th {
+ text-align: left;
+ padding: 12px 16px;
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--fg-muted);
+ background: var(--bg-hover);
+ border-bottom: 1px solid var(--border);
+}
+
+.data-table td {
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--border-light);
+ font-size: 13px;
+}
+
+.data-table tr:last-child td {
+ border-bottom: none;
+}
+
+.data-table tr:hover td {
+ background: hsl(0 0% 50% / 0.03);
+}
+
+.status-badge {
+ display: inline-block;
+ padding: 2px 10px;
+ border-radius: 999px;
+ font-size: 11px;
+ font-weight: 600;
+}
+
+.status-badge.aktiv {
+ background: hsl(142 71% 45% / 0.15);
+ color: var(--green);
+}
+
+.status-badge.inaktiv {
+ background: hsl(0 0% 50% / 0.1);
+ color: var(--fg-muted);
+}
+
+.color-dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ display: inline-block;
+ flex-shrink: 0;
+}
+
+.rep-tag {
+ font-size: 11px;
+ background: var(--bg-hover);
+ padding: 2px 8px;
+ border-radius: var(--radius);
+ font-family: var(--font);
+}
+
+/* ββ Add Form Row ββββββββββββββββββββββββββββββββββββ */
+.add-form {
+ display: flex;
+ gap: 8px;
+ align-items: flex-end;
+ flex-wrap: wrap;
+ margin-bottom: 20px;
+ padding: 16px;
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+}
+
+.add-form .field {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.add-form .field label {
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--fg-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.add-form input,
+.add-form select {
+ min-width: 140px;
+}
+
+/* ββ Gantt Chart βββββββββββββββββββββββββββββββββββββ */
+.gantt-container {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ font-family: var(--font);
+ font-size: 11px;
+}
+
+.gantt-scroll-window {
+ position: relative;
+ overflow-x: auto;
+ border-top: 1px solid var(--border);
+ min-height: 400px;
+}
+
+.gantt-header {
+ position: sticky;
+ top: 0;
+ z-index: 20;
+ height: 36px;
+ background: hsl(0 0% 0% / 0.04);
+ border-bottom: 1px solid var(--border-light);
+ margin-left: 170px;
+ /* Space for labels */
+}
+
+[data-theme="light"] .gantt-header {
+ background: hsl(0 0% 0% / 0.02);
+}
+
+.gantt-tick {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ border-left: 1px solid var(--border-root);
+ border-left: 1px solid hsl(0 0% 50% / 0.2);
+ padding-left: 4px;
+ padding-top: 8px;
+}
+
+.gantt-tick-label {
+ font-size: 10px;
+ color: var(--fg-muted);
+ font-weight: 600;
+ transform: translateX(-50%);
+ display: inline-block;
+ white-space: nowrap;
+}
+
+.gantt-body {
+ position: relative;
+ min-height: 100px;
+ padding-bottom: 80px;
+ /* buffer */
+}
+
+.gantt-now-line {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 2px;
+ background-color: var(--green);
+ z-index: 100;
+ box-shadow: 0 0 8px var(--green);
+ opacity: 0.8;
+ pointer-events: none;
+}
+
+.gantt-row-group {
+ margin-bottom: 8px;
+}
+
+.gantt-group-header {
+ background: hsl(0 0% 0% / 0.08);
+ font-weight: 700;
+ font-size: 12px;
+ padding: 8px 16px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--fg);
+ border-bottom: 1px solid var(--border-light);
+ border-top: 1px solid var(--border-light);
+}
+
+[data-theme="light"] .gantt-group-header {
+ background: hsl(0 0% 0% / 0.03);
+}
+
+.gantt-row {
+ display: flex;
+ min-height: 44px;
+ border-bottom: 1px dashed hsl(0 0% 50% / 0.15);
+ background: transparent;
+}
+
+.gantt-row:hover {
+ background: hsl(0 0% 50% / 0.03);
+}
+
+.gantt-row-info {
+ width: 170px;
+ flex-shrink: 0;
+ padding: 8px 12px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ border-right: 1px solid var(--border-light);
+ background: var(--bg-card);
+ position: sticky;
+ left: 0;
+ z-index: 15;
+}
+
+.gantt-bars-area {
+ flex: 1;
+ position: relative;
+ min-height: 44px;
+}
+
+.gantt-block {
+ position: absolute;
+ top: 8px;
+ bottom: 8px;
+ border-radius: 4px;
+ min-width: 2px;
+ z-index: 10;
+ display: flex;
+ align-items: center;
+ overflow: hidden;
+ padding: 0 4px;
+ transition: transform 100ms ease;
+ cursor: pointer;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+.gantt-block:hover {
+ z-index: 50;
+ transform: scaleY(1.1);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
+ filter: brightness(1.2);
+}
+
+.gantt-block-label {
+ font-size: 10px;
+ color: #fff;
+ font-weight: 600;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);
+ white-space: nowrap;
+ pointer-events: none;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/* ββ Empty State βββββββββββββββββββββββββββββββββββββ */
+.empty-state {
+ grid-column: 1 / -1;
+ text-align: center;
+ padding: 48px;
+ color: var(--fg-muted);
+ font-size: 13px;
+ border: 1px dashed var(--border);
+ border-radius: var(--radius-lg);
+}
+
+/* ββ Loading βββββββββββββββββββββββββββββββββββββββββ */
+.loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 64px;
+ color: var(--fg-muted);
+ font-size: 13px;
+ gap: 8px;
+}
+
+.spinner {
+ width: 16px;
+ height: 16px;
+ border: 2px solid var(--border);
+ border-top-color: var(--primary);
+ border-radius: 50%;
+ animation: spin 0.6s linear infinite;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
\ No newline at end of file
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..38d3698
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,13 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import { BrowserRouter } from 'react-router-dom'
+import App from './App'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+
+
+)
diff --git a/src/pages/Processes.tsx b/src/pages/Processes.tsx
new file mode 100644
index 0000000..7835aa1
--- /dev/null
+++ b/src/pages/Processes.tsx
@@ -0,0 +1,307 @@
+import { useState, useEffect, useCallback } from 'react'
+import { api, type Process, type Server } from '../api/baserow'
+
+const REPETITIONS = [
+ 'TAEGLICH', 'STUENDLICH', 'INTERVALL', 'WERKTAGE', 'WOCHENENDE',
+ 'WOECHENTLICH', 'MONATLICH_FESTER_TAG', 'BENUTZERDEFINIERT'
+]
+
+const MAX_VIEWS = [
+ 'Immer',
+ 'Max 1 Woche',
+ 'Max 1 Tag',
+ 'Max 6 Stunden'
+]
+
+export default function Processes() {
+ const [processes, setProcesses] = useState([])
+ const [servers, setServers] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [saving, setSaving] = useState(false)
+
+ // Form state
+ const [editingId, setEditingId] = useState(null)
+ const [name, setName] = useState('')
+ const [serverId, setServerId] = useState('')
+ const [status, setStatus] = useState('aktiv')
+ const [rep, setRep] = useState('TAEGLICH')
+ const [color, setColor] = useState('#3b82f6')
+ const [sichtbarkeit, setSichtbarkeit] = useState('Immer')
+
+ // Conditional fields
+ const [startTime, setStartTime] = useState('')
+ const [endTime, setEndTime] = useState('')
+ const [firstExec, setFirstExec] = useState('')
+ const [execUntil, setExecUntil] = useState('')
+ const [intervalBetween, setIntervalBetween] = useState('')
+ const [startMinute, setStartMinute] = useState('')
+ const [duration, setDuration] = useState('')
+ const [weekdays, setWeekdays] = useState('')
+
+ const load = useCallback(async () => {
+ try {
+ const [p, s] = await Promise.all([api.getProcesses(), api.getServers()])
+ setProcesses(p)
+ setServers(s)
+ } finally {
+ setLoading(false)
+ }
+ }, [])
+
+ useEffect(() => { load() }, [load])
+
+ const editProcess = (p: Process) => {
+ setEditingId(p.id)
+ setName(p.Name || '')
+ setServerId(p.Server?.[0]?.id || '')
+ setStatus(p.Status?.value || 'aktiv')
+ setRep(p.Wiederholung?.value || 'TAEGLICH')
+ setColor(p.Farbe || '#3b82f6')
+ setSichtbarkeit(p.Sichtbarkeit?.value || 'Immer')
+
+ setStartTime(p.Start || '')
+ setEndTime(p.Ende || '')
+ setFirstExec(p['Erste AusfΓΌhrung'] || '')
+ setExecUntil(p['AusfΓΌhrung bis'] || '')
+ setIntervalBetween(p['Intervall zwischen'] || '')
+ setStartMinute(p['Start Minute'] !== null && p['Start Minute'] !== undefined ? String(p['Start Minute']) : '')
+ setDuration(p.Dauer || '')
+ setWeekdays(p.Wochentage || '')
+
+ window.scrollTo({ top: 0, behavior: 'smooth' })
+ }
+
+ const cancelEdit = () => {
+ setEditingId(null)
+ setName('')
+ setStartTime('')
+ setEndTime('')
+ setFirstExec('')
+ setExecUntil('')
+ setIntervalBetween('')
+ setStartMinute('')
+ setDuration('')
+ setWeekdays('')
+ setSichtbarkeit('Immer')
+ }
+
+ const handleSave = async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!name.trim() || !serverId) return
+ setSaving(true)
+
+ const payload: any = {
+ Name: name.trim(),
+ Server: [serverId],
+ Status: status,
+ Wiederholung: rep,
+ Farbe: color,
+ Sichtbarkeit: sichtbarkeit,
+ Dauer: duration || null,
+ Wochentage: weekdays || null,
+ Start: startTime || null,
+ Ende: endTime || null,
+ 'Start Minute': startMinute ? parseInt(startMinute) : null,
+ 'Erste AusfΓΌhrung': firstExec || null,
+ 'AusfΓΌhrung bis': execUntil || null,
+ 'Intervall zwischen': intervalBetween || null,
+ }
+
+ try {
+ if (editingId) {
+ await api.updateProcess(editingId, payload)
+ } else {
+ const maxSort = processes.reduce((max, p) => Math.max(max, p.Sortierung ?? 0), 0)
+ payload.Sortierung = maxSort + 1
+ await api.createProcess(payload)
+ }
+ cancelEdit()
+ await load()
+ } catch (err: any) {
+ alert('Fehler beim Speichern. Hast du das Feld "Max View" in Baserow als Single-Select mit den richtigen Werten (z.B. "Immer") angelegt?\n\nDetails: ' + err.message)
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const handleDelete = async (id: number, procName: string) => {
+ if (!confirm(`Prozess "${procName}" wirklich lΓΆschen?`)) return
+ await api.deleteProcess(id)
+ await load()
+ }
+
+ if (loading) {
+ return Ladenβ¦
+ }
+
+ return (
+
+
+
+
Prozesse
+
Automatisierte Scripts und Backup-Tasks
+
+
+
+ {/* Add/Edit form */}
+
+
+ {/* Process table */}
+ {processes.length > 0 ? (() => {
+ const sortedProcesses = processes.slice().sort((a, b) => {
+ const sNameA = a.Server?.[0]?.value || ''
+ const sA = servers.find(s => s.Name === sNameA)
+ const sortA = sA?.Sortierung ?? 9999
+
+ const sNameB = b.Server?.[0]?.value || ''
+ const sB = servers.find(s => s.Name === sNameB)
+ const sortB = sB?.Sortierung ?? 9999
+
+ if (sortA !== sortB) return sortA - sortB
+ return (a.Sortierung ?? 9999) - (b.Sortierung ?? 9999)
+ })
+
+ return (
+
+
+
+ | Status |
+ Server |
+ Prozessname |
+ Wiederholung |
+ Farbe |
+ Sichtbarkeit |
+ Aktionen |
+
+
+
+ {sortedProcesses.map(p => (
+
+ |
+
+ {p.Status?.value || 'β'}
+
+ |
+ {p.Server?.[0]?.value || 'β'} |
+
+
+ {p.Name || '(Kein Name)'}
+
+ |
+ {p.Wiederholung?.value || 'β'} |
+ |
+ {p.Sichtbarkeit?.value || 'Immer'} |
+
+
+
+ |
+
+ ))}
+
+
+ )
+ })() : (
+
Noch keine Prozesse angelegt.
+ )}
+
+ )
+}
diff --git a/src/pages/Servers.tsx b/src/pages/Servers.tsx
new file mode 100644
index 0000000..e5bd0ff
--- /dev/null
+++ b/src/pages/Servers.tsx
@@ -0,0 +1,105 @@
+import { useState, useEffect, useCallback } from 'react'
+import { api, type Server } from '../api/baserow'
+
+export default function Servers() {
+ const [servers, setServers] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [name, setName] = useState('')
+ const [desc, setDesc] = useState('')
+ const [saving, setSaving] = useState(false)
+
+ const load = useCallback(async () => {
+ try {
+ setServers(await api.getServers())
+ } finally {
+ setLoading(false)
+ }
+ }, [])
+
+ useEffect(() => { load() }, [load])
+
+ const handleAdd = async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!name.trim()) return
+ setSaving(true)
+ try {
+ await api.createServer(name.trim(), desc.trim())
+ setName('')
+ setDesc('')
+ await load()
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ const handleDelete = async (id: number, serverName: string) => {
+ if (!confirm(`Server "${serverName}" wirklich lΓΆschen?`)) return
+ await api.deleteServer(id)
+ await load()
+ }
+
+ if (loading) {
+ return Ladenβ¦
+ }
+
+ return (
+
+
+
+
Server
+
Server verwalten und ΓΌberwachen
+
+
+
+ {/* Add form */}
+
+
+ {/* Server cards */}
+
+ {servers.map(s => (
+
+
{s.Name || '(Kein Name)'}
+ {s.Beschreibung &&
{s.Beschreibung}
}
+
+ {s.Prozesse?.length || 0} Prozesse
+
+
+
+ ))}
+ {servers.length === 0 && (
+
+ Noch keine Server konfiguriert. FΓΌge einen hinzu!
+
+ )}
+
+
+ )
+}
diff --git a/src/pages/Timeline.tsx b/src/pages/Timeline.tsx
new file mode 100644
index 0000000..b18a5f8
--- /dev/null
+++ b/src/pages/Timeline.tsx
@@ -0,0 +1,84 @@
+import { useState, useEffect, useCallback } from 'react'
+import { Link } from 'react-router-dom'
+import { api, type Server, type Process } from '../api/baserow'
+import GanttChart from '../components/GanttChart'
+
+export default function Timeline() {
+ const [servers, setServers] = useState([])
+ const [processes, setProcesses] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [runningCount, setRunningCount] = useState(0)
+
+ const load = useCallback(async () => {
+ try {
+ const [s, p] = await Promise.all([api.getServers(), api.getProcesses()])
+ setServers(s)
+ setProcesses(p)
+ } catch (e) {
+ console.error('Failed to load data:', e)
+ } finally {
+ setLoading(false)
+ }
+ }, [])
+
+ useEffect(() => {
+ load()
+ const interval = setInterval(load, 30_000) // Auto-refresh every 30s
+ return () => clearInterval(interval)
+ }, [load])
+
+ const activeCount = processes.filter(p => p.Status?.value === 'aktiv').length
+
+ if (loading) {
+ return Daten werden geladenβ¦
+ }
+
+ return (
+
+
+
+
Prozess-Zeitplan
+
24-Stunden Γbersicht aller Server-Prozesse
+
+
+ Server verwalten
+
+
+ {/* KPI Cards */}
+
+
+
+
+ )
+}
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/start.bat b/start.bat
new file mode 100644
index 0000000..37bb554
--- /dev/null
+++ b/start.bat
@@ -0,0 +1,5 @@
+@echo off
+echo Starting AZion Scheduler...
+set "PATH=%LOCALAPPDATA%\nodejs-portable\node-v22.15.0-win-x64;%PATH%"
+npm run dev
+pause
diff --git a/test-browser.js b/test-browser.js
new file mode 100644
index 0000000..eece357
--- /dev/null
+++ b/test-browser.js
@@ -0,0 +1,10 @@
+const puppeteer = require('puppeteer');
+(async () => {
+ const browser = await puppeteer.launch();
+ const page = await browser.newPage();
+ page.on('console', msg => console.log('PAGE LOG:', msg.text()));
+ page.on('pageerror', error => console.log('PAGE ERROR:', error.message));
+ page.on('requestfailed', request => console.log('REQUEST FAILED:', request.url(), request.failure().errorText));
+ await page.goto('http://localhost:3001', {waitUntil: 'networkidle0'}).catch(e => console.log('GOTO ERROR:', e.message));
+ await browser.close();
+})();
diff --git a/test-time.js b/test-time.js
new file mode 100644
index 0000000..45d6869
--- /dev/null
+++ b/test-time.js
@@ -0,0 +1,9 @@
+function parseTime(tStr) {
+ if (!tStr) return [12, 0]
+ const p = tStr.toString().split(':').map(Number)
+ return [isNaN(p[0]) ? 12 : p[0], isNaN(p[1]) ? 0 : p[1]]
+}
+
+console.log(parseTime('00:00:30'));
+console.log(parseTime('07:23'));
+console.log(parseTime(''));
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..75e2ff7
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noFallthroughCasesInSwitch": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": ["src"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..3244652
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 3001,
+ watch: {
+ usePolling: true
+ }
+ },
+ resolve: {
+ preserveSymlinks: true
+ }
+})