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 */} +
+
+
+ + setName(e.target.value)} placeholder="Prozessname" required /> +
+
+ + +
+
+ + +
+
+ + +
+
+ + setColor(e.target.value)} style={{ width: 40, padding: 2, height: 34 }} /> +
+
+ + +
+
+ + {/* Dynamic Fields row based on chosen Wiederholung */} +
+ {/* Default Start */} + {(rep === 'TAEGLICH' || rep === 'WOECHENTLICH' || rep === 'WERKTAGE' || rep === 'WOCHENENDE' || rep === 'MONATLICH_FESTER_TAG' || rep === 'BENUTZERDEFINIERT') && ( +
+ + setStartTime(e.target.value)} placeholder="08:00:00" style={{ width: 120 }} /> +
+ )} + + {/* Stuendlich */} + {rep === 'STUENDLICH' && ( +
+ + setStartMinute(e.target.value)} placeholder="45" style={{ width: 100 }} /> +
+ )} + + {/* Intervall */} + {rep === 'INTERVALL' && ( + <> +
+ + setFirstExec(e.target.value)} placeholder="00:00:00" style={{ width: 120 }} /> +
+
+ + setExecUntil(e.target.value)} placeholder="23:59:59" style={{ width: 120 }} /> +
+
+ + setIntervalBetween(e.target.value)} placeholder="3600" style={{ width: 120 }} /> +
+ + )} + + {/* Universal Dauer */} +
+ + setDuration(e.target.value)} placeholder="60" style={{ width: 120 }} /> +
+ + {/* Wochentage */} + {(rep === 'WOECHENTLICH' || rep === 'INTERVALL' || rep === 'BENUTZERDEFINIERT') && ( +
+ + setWeekdays(e.target.value)} placeholder="Mo,Di,Mi,Do,Fr" style={{ width: 140 }} /> +
+ )} +
+ +
+ + {editingId && ( + + )} +
+
+ + {/* 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 ( + + + + + + + + + + + + + + {sortedProcesses.map(p => ( + + + + + + + + + + ))} + +
StatusServerProzessnameWiederholungFarbeSichtbarkeitAktionen
+ + {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 */} +
+
+ + setName(e.target.value)} + placeholder="Server-Name" + required + /> +
+
+ + setDesc(e.target.value)} + placeholder="Optional" + /> +
+ +
+ + {/* 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 */} +
+
+
+ + Server +
+
{servers.length}
+
+ +
+
+ + Prozesse gesamt +
+
{processes.length}
+
+ +
+
+ + Aktive Prozesse +
+
{activeCount}
+
+ +
+
+ + Laufen gerade +
+
{runningCount}
+
+
+ + +
+ ) +} 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 + } +})