From 9f6219832c93091b5479a550b0bf3d5136492915 Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Fri, 23 May 2025 07:24:51 +0200 Subject: [PATCH] "feat: Added debug server and related components for improved development experience" --- .gitattributes | 47 - .gitignore | 457 +- PROJECT_STRUCTURE.md | 1 + backend/app.py | 176 +- backend/cleanup.sh | 218 + backend/config.py | 145 + .../static/css/debug-dashboard.css | 617 ++ .../debug-server/static/js/debug-charts.js | 470 + .../debug-server/static/js/debug-dashboard.js | 1234 +++ backend/debug-server/templates/dashboard.html | 444 + backend/debug-server/templates/debug.html | 261 + backend/frontend_v2_routes.py | 346 + backend/install.sh | 510 + backend/network_config.py | 185 + backend/requirements.txt | 27 +- backend/security.py | 1 + backend/start-debug-server.bat | 36 + backend/start-debug-server.sh | 52 + backend/start-production.bat | 38 + backend/start-production.sh | 56 + backend/templates/network_config.html | 119 + backend/wsgi.py | 37 + cleanup.ps1 | 50 + cleanup.sh | 42 + config/README.md | 80 + config/install-linux-service.sh | 58 + config/install-windows-service.bat | 66 + config/myp-service.service | 14 + docker-compose.dev.yml | 110 + docker-compose.yml | 95 + frontend/.gitignore | 43 + frontend/Dockerfile | 34 + frontend/README.md | 32 + frontend/biome.json | 19 + frontend/cleanup.sh | 245 + frontend/components.json | 17 + frontend/debug-server/package.json | 18 + frontend/debug-server/public/css/style.css | 417 + frontend/debug-server/public/js/script.js | 505 + frontend/debug-server/public/views/index.ejs | 227 + frontend/debug-server/src/app.js | 235 + frontend/debug-server/src/index.js | 471 + frontend/docker-compose.yml | 51 + frontend/docker/build.sh | 31 + frontend/docker/caddy/Caddyfile | 52 + frontend/docker/compose.yml | 30 + frontend/docker/deploy.sh | 36 + frontend/docker/images/.gitattributes | 2 + frontend/docker/save.sh | 68 + frontend/docs/Admin-Dashboard.md | 116 + frontend/docs/Architektur.md | 79 + frontend/docs/Bereitstellungsdetails .md | 150 + frontend/docs/Datenbank.md | 153 + frontend/docs/Installation.md | 93 + frontend/docs/Nutzung.md | 75 + frontend/docs/README.md | 37 + frontend/drizzle.config.ts | 12 + .../drizzle/0000_overjoyed_strong_guy.sql | 35 + frontend/drizzle/meta/0000_snapshot.json | 241 + frontend/drizzle/meta/_journal.json | 13 + frontend/https-setup.sh | 376 + frontend/install.sh | 570 + frontend/next.config.mjs | 26 + frontend/package.json | 83 + frontend/pnpm-lock.yaml | 5707 ++++++++++ frontend/postcss.config.mjs | 8 + frontend/public/next.svg | 1 + frontend/public/vercel.svg | 1 + frontend/repomix-output.txt | 9279 +++++++++++++++++ frontend/scripts/generate-data.js | 367 + frontend/setup-backend-url.sh | 82 + frontend/src/app/admin/about/page.tsx | 32 + frontend/src/app/admin/admin-sidebar.tsx | 66 + .../app/admin/charts/printer-error-chart.tsx | 68 + .../app/admin/charts/printer-error-rate.tsx | 66 + .../src/app/admin/charts/printer-forecast.tsx | 83 + .../app/admin/charts/printer-utilization.tsx | 80 + .../src/app/admin/charts/printer-volume.tsx | 69 + frontend/src/app/admin/jobs/page.tsx | 35 + frontend/src/app/admin/layout.tsx | 34 + frontend/src/app/admin/page.tsx | 121 + frontend/src/app/admin/printers/columns.tsx | 86 + .../src/app/admin/printers/data-table.tsx | 135 + .../admin/printers/dialogs/create-printer.tsx | 26 + .../admin/printers/dialogs/delete-printer.tsx | 83 + .../admin/printers/dialogs/edit-printer.tsx | 30 + frontend/src/app/admin/printers/form.tsx | 204 + frontend/src/app/admin/printers/page.tsx | 31 + .../src/app/admin/settings/download/route.ts | 7 + frontend/src/app/admin/settings/page.tsx | 30 + frontend/src/app/admin/users/columns.tsx | 137 + frontend/src/app/admin/users/data-table.tsx | 135 + frontend/src/app/admin/users/dialog.tsx | 56 + frontend/src/app/admin/users/form.tsx | 212 + frontend/src/app/admin/users/page.tsx | 26 + .../api/job/[jobId]/remaining-time/route.ts | 41 + frontend/src/app/api/jobs/[id]/route.ts | 99 + frontend/src/app/api/jobs/route.ts | 59 + frontend/src/app/api/printers/route.ts | 27 + frontend/src/app/auth/login/callback/route.ts | 123 + frontend/src/app/auth/login/route.ts | 30 + frontend/src/app/favicon.ico | Bin 0 -> 169755 bytes frontend/src/app/globals.css | 61 + frontend/src/app/job/[jobId]/cancel-form.tsx | 132 + .../src/app/job/[jobId]/edit-comments.tsx | 56 + frontend/src/app/job/[jobId]/extend-form.tsx | 151 + frontend/src/app/job/[jobId]/finish-form.tsx | 87 + frontend/src/app/job/[jobId]/page.tsx | 123 + frontend/src/app/layout.tsx | 36 + frontend/src/app/my/jobs/columns.tsx | 141 + frontend/src/app/my/jobs/data-table.tsx | 73 + frontend/src/app/my/profile/page.tsx | 47 + frontend/src/app/not-found.tsx | 11 + frontend/src/app/page.tsx | 71 + .../app/printer/[printerId]/reserve/form.tsx | 169 + .../app/printer/[printerId]/reserve/page.tsx | 36 + frontend/src/components/data-card.tsx | 38 + frontend/src/components/data-table.tsx | 0 .../src/components/dynamic-printer-cards.tsx | 38 + frontend/src/components/header/index.tsx | 85 + frontend/src/components/header/navigation.tsx | 49 + frontend/src/components/login-button.tsx | 37 + frontend/src/components/logout-button.tsx | 23 + .../src/components/personalized-cards.tsx | 71 + .../components/printer-availability-badge.tsx | 25 + .../src/components/printer-card/countdown.tsx | 44 + .../src/components/printer-card/index.tsx | 87 + frontend/src/components/ui/alert-dialog.tsx | 141 + frontend/src/components/ui/alert.tsx | 59 + frontend/src/components/ui/avatar.tsx | 50 + frontend/src/components/ui/badge.tsx | 36 + frontend/src/components/ui/breadcrumb.tsx | 115 + frontend/src/components/ui/button.tsx | 57 + frontend/src/components/ui/card.tsx | 76 + frontend/src/components/ui/chart.tsx | 370 + frontend/src/components/ui/dialog.tsx | 122 + frontend/src/components/ui/dropdown-menu.tsx | 205 + frontend/src/components/ui/form.tsx | 176 + frontend/src/components/ui/hover-card.tsx | 29 + frontend/src/components/ui/input.tsx | 25 + frontend/src/components/ui/label.tsx | 26 + frontend/src/components/ui/scroll-area.tsx | 48 + frontend/src/components/ui/select.tsx | 164 + frontend/src/components/ui/skeleton.tsx | 15 + frontend/src/components/ui/sonner.tsx | 31 + frontend/src/components/ui/table.tsx | 120 + frontend/src/components/ui/tabs.tsx | 55 + frontend/src/components/ui/textarea.tsx | 24 + frontend/src/components/ui/toast.tsx | 129 + frontend/src/components/ui/toaster.tsx | 35 + frontend/src/components/ui/use-toast.ts | 194 + .../server/actions/authentication/logout.ts | 29 + frontend/src/server/actions/printJobs.ts | 305 + frontend/src/server/actions/printers.ts | 130 + frontend/src/server/actions/timer.ts | 7 + frontend/src/server/actions/user/delete.ts | 56 + frontend/src/server/actions/user/update.ts | 41 + frontend/src/server/actions/users.ts | 80 + frontend/src/server/auth/index.ts | 72 + frontend/src/server/auth/oauth.ts | 54 + frontend/src/server/auth/permissions.ts | 28 + frontend/src/utils/analytics/error-rate.ts | 54 + frontend/src/utils/analytics/errors.ts | 39 + frontend/src/utils/analytics/forecast.ts | 97 + frontend/src/utils/analytics/utilization.ts | 32 + frontend/src/utils/analytics/volume.ts | 52 + frontend/src/utils/api-config.ts | 51 + frontend/src/utils/drizzle.ts | 29 + frontend/src/utils/errors.ts | 59 + frontend/src/utils/external-api.ts | 78 + frontend/src/utils/fetch.ts | 1 + frontend/src/utils/guard.ts | 35 + frontend/src/utils/printers.ts | 41 + frontend/src/utils/strings.ts | 11 + frontend/src/utils/styles.ts | 11 + frontend/start-debug-server.bat | 46 + frontend/start-debug-server.sh | 55 + frontend/tailwind.config.ts | 208 + frontend/tsconfig.json | 26 + frontend/update-package.js | 179 + infrastructure/environments/development.env | 63 + infrastructure/environments/test.env | 81 + infrastructure/scripts/cleanup.ps1 | 256 + infrastructure/scripts/cleanup.sh | 289 + infrastructure/scripts/start.ps1 | 242 + infrastructure/scripts/start.sh | 296 + proxy/Caddyfile | 229 + start.ps1 | 137 + start.sh | 111 + 189 files changed, 35730 insertions(+), 133 deletions(-) delete mode 100644 .gitattributes create mode 100644 PROJECT_STRUCTURE.md create mode 100644 backend/cleanup.sh create mode 100644 backend/config.py create mode 100644 backend/debug-server/static/css/debug-dashboard.css create mode 100644 backend/debug-server/static/js/debug-charts.js create mode 100644 backend/debug-server/static/js/debug-dashboard.js create mode 100644 backend/debug-server/templates/dashboard.html create mode 100644 backend/debug-server/templates/debug.html create mode 100644 backend/frontend_v2_routes.py create mode 100644 backend/install.sh create mode 100644 backend/network_config.py create mode 100644 backend/security.py create mode 100644 backend/start-debug-server.bat create mode 100644 backend/start-debug-server.sh create mode 100644 backend/start-production.bat create mode 100644 backend/start-production.sh create mode 100644 backend/templates/network_config.html create mode 100644 backend/wsgi.py create mode 100644 cleanup.ps1 create mode 100644 cleanup.sh create mode 100644 config/README.md create mode 100644 config/install-linux-service.sh create mode 100644 config/install-windows-service.bat create mode 100644 config/myp-service.service create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.yml create mode 100644 frontend/.gitignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/biome.json create mode 100644 frontend/cleanup.sh create mode 100644 frontend/components.json create mode 100644 frontend/debug-server/package.json create mode 100644 frontend/debug-server/public/css/style.css create mode 100644 frontend/debug-server/public/js/script.js create mode 100644 frontend/debug-server/public/views/index.ejs create mode 100644 frontend/debug-server/src/app.js create mode 100644 frontend/debug-server/src/index.js create mode 100644 frontend/docker-compose.yml create mode 100644 frontend/docker/build.sh create mode 100644 frontend/docker/caddy/Caddyfile create mode 100644 frontend/docker/compose.yml create mode 100644 frontend/docker/deploy.sh create mode 100644 frontend/docker/images/.gitattributes create mode 100644 frontend/docker/save.sh create mode 100644 frontend/docs/Admin-Dashboard.md create mode 100644 frontend/docs/Architektur.md create mode 100644 frontend/docs/Bereitstellungsdetails .md create mode 100644 frontend/docs/Datenbank.md create mode 100644 frontend/docs/Installation.md create mode 100644 frontend/docs/Nutzung.md create mode 100644 frontend/docs/README.md create mode 100644 frontend/drizzle.config.ts create mode 100644 frontend/drizzle/0000_overjoyed_strong_guy.sql create mode 100644 frontend/drizzle/meta/0000_snapshot.json create mode 100644 frontend/drizzle/meta/_journal.json create mode 100644 frontend/https-setup.sh create mode 100644 frontend/install.sh create mode 100644 frontend/next.config.mjs create mode 100644 frontend/package.json create mode 100644 frontend/pnpm-lock.yaml create mode 100644 frontend/postcss.config.mjs create mode 100644 frontend/public/next.svg create mode 100644 frontend/public/vercel.svg create mode 100644 frontend/repomix-output.txt create mode 100644 frontend/scripts/generate-data.js create mode 100644 frontend/setup-backend-url.sh create mode 100644 frontend/src/app/admin/about/page.tsx create mode 100644 frontend/src/app/admin/admin-sidebar.tsx create mode 100644 frontend/src/app/admin/charts/printer-error-chart.tsx create mode 100644 frontend/src/app/admin/charts/printer-error-rate.tsx create mode 100644 frontend/src/app/admin/charts/printer-forecast.tsx create mode 100644 frontend/src/app/admin/charts/printer-utilization.tsx create mode 100644 frontend/src/app/admin/charts/printer-volume.tsx create mode 100644 frontend/src/app/admin/jobs/page.tsx create mode 100644 frontend/src/app/admin/layout.tsx create mode 100644 frontend/src/app/admin/page.tsx create mode 100644 frontend/src/app/admin/printers/columns.tsx create mode 100644 frontend/src/app/admin/printers/data-table.tsx create mode 100644 frontend/src/app/admin/printers/dialogs/create-printer.tsx create mode 100644 frontend/src/app/admin/printers/dialogs/delete-printer.tsx create mode 100644 frontend/src/app/admin/printers/dialogs/edit-printer.tsx create mode 100644 frontend/src/app/admin/printers/form.tsx create mode 100644 frontend/src/app/admin/printers/page.tsx create mode 100644 frontend/src/app/admin/settings/download/route.ts create mode 100644 frontend/src/app/admin/settings/page.tsx create mode 100644 frontend/src/app/admin/users/columns.tsx create mode 100644 frontend/src/app/admin/users/data-table.tsx create mode 100644 frontend/src/app/admin/users/dialog.tsx create mode 100644 frontend/src/app/admin/users/form.tsx create mode 100644 frontend/src/app/admin/users/page.tsx create mode 100644 frontend/src/app/api/job/[jobId]/remaining-time/route.ts create mode 100644 frontend/src/app/api/jobs/[id]/route.ts create mode 100644 frontend/src/app/api/jobs/route.ts create mode 100644 frontend/src/app/api/printers/route.ts create mode 100644 frontend/src/app/auth/login/callback/route.ts create mode 100644 frontend/src/app/auth/login/route.ts create mode 100644 frontend/src/app/favicon.ico create mode 100644 frontend/src/app/globals.css create mode 100644 frontend/src/app/job/[jobId]/cancel-form.tsx create mode 100644 frontend/src/app/job/[jobId]/edit-comments.tsx create mode 100644 frontend/src/app/job/[jobId]/extend-form.tsx create mode 100644 frontend/src/app/job/[jobId]/finish-form.tsx create mode 100644 frontend/src/app/job/[jobId]/page.tsx create mode 100644 frontend/src/app/layout.tsx create mode 100644 frontend/src/app/my/jobs/columns.tsx create mode 100644 frontend/src/app/my/jobs/data-table.tsx create mode 100644 frontend/src/app/my/profile/page.tsx create mode 100644 frontend/src/app/not-found.tsx create mode 100644 frontend/src/app/page.tsx create mode 100644 frontend/src/app/printer/[printerId]/reserve/form.tsx create mode 100644 frontend/src/app/printer/[printerId]/reserve/page.tsx create mode 100644 frontend/src/components/data-card.tsx create mode 100644 frontend/src/components/data-table.tsx create mode 100644 frontend/src/components/dynamic-printer-cards.tsx create mode 100644 frontend/src/components/header/index.tsx create mode 100644 frontend/src/components/header/navigation.tsx create mode 100644 frontend/src/components/login-button.tsx create mode 100644 frontend/src/components/logout-button.tsx create mode 100644 frontend/src/components/personalized-cards.tsx create mode 100644 frontend/src/components/printer-availability-badge.tsx create mode 100644 frontend/src/components/printer-card/countdown.tsx create mode 100644 frontend/src/components/printer-card/index.tsx create mode 100644 frontend/src/components/ui/alert-dialog.tsx create mode 100644 frontend/src/components/ui/alert.tsx create mode 100644 frontend/src/components/ui/avatar.tsx create mode 100644 frontend/src/components/ui/badge.tsx create mode 100644 frontend/src/components/ui/breadcrumb.tsx create mode 100644 frontend/src/components/ui/button.tsx create mode 100644 frontend/src/components/ui/card.tsx create mode 100644 frontend/src/components/ui/chart.tsx create mode 100644 frontend/src/components/ui/dialog.tsx create mode 100644 frontend/src/components/ui/dropdown-menu.tsx create mode 100644 frontend/src/components/ui/form.tsx create mode 100644 frontend/src/components/ui/hover-card.tsx create mode 100644 frontend/src/components/ui/input.tsx create mode 100644 frontend/src/components/ui/label.tsx create mode 100644 frontend/src/components/ui/scroll-area.tsx create mode 100644 frontend/src/components/ui/select.tsx create mode 100644 frontend/src/components/ui/skeleton.tsx create mode 100644 frontend/src/components/ui/sonner.tsx create mode 100644 frontend/src/components/ui/table.tsx create mode 100644 frontend/src/components/ui/tabs.tsx create mode 100644 frontend/src/components/ui/textarea.tsx create mode 100644 frontend/src/components/ui/toast.tsx create mode 100644 frontend/src/components/ui/toaster.tsx create mode 100644 frontend/src/components/ui/use-toast.ts create mode 100644 frontend/src/server/actions/authentication/logout.ts create mode 100644 frontend/src/server/actions/printJobs.ts create mode 100644 frontend/src/server/actions/printers.ts create mode 100644 frontend/src/server/actions/timer.ts create mode 100644 frontend/src/server/actions/user/delete.ts create mode 100644 frontend/src/server/actions/user/update.ts create mode 100644 frontend/src/server/actions/users.ts create mode 100644 frontend/src/server/auth/index.ts create mode 100644 frontend/src/server/auth/oauth.ts create mode 100644 frontend/src/server/auth/permissions.ts create mode 100644 frontend/src/utils/analytics/error-rate.ts create mode 100644 frontend/src/utils/analytics/errors.ts create mode 100644 frontend/src/utils/analytics/forecast.ts create mode 100644 frontend/src/utils/analytics/utilization.ts create mode 100644 frontend/src/utils/analytics/volume.ts create mode 100644 frontend/src/utils/api-config.ts create mode 100644 frontend/src/utils/drizzle.ts create mode 100644 frontend/src/utils/errors.ts create mode 100644 frontend/src/utils/external-api.ts create mode 100644 frontend/src/utils/fetch.ts create mode 100644 frontend/src/utils/guard.ts create mode 100644 frontend/src/utils/printers.ts create mode 100644 frontend/src/utils/strings.ts create mode 100644 frontend/src/utils/styles.ts create mode 100644 frontend/start-debug-server.bat create mode 100644 frontend/start-debug-server.sh create mode 100644 frontend/tailwind.config.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/update-package.js create mode 100644 infrastructure/environments/development.env create mode 100644 infrastructure/environments/test.env create mode 100644 infrastructure/scripts/cleanup.ps1 create mode 100644 infrastructure/scripts/cleanup.sh create mode 100644 infrastructure/scripts/start.ps1 create mode 100644 infrastructure/scripts/start.sh create mode 100644 proxy/Caddyfile create mode 100644 start.ps1 create mode 100644 start.sh diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index b1e7a502..00000000 --- a/.gitattributes +++ /dev/null @@ -1,47 +0,0 @@ -# Standard: alles als Text behandeln und LF speichern -* text=auto eol=lf - -# Beispiel für Ausnahmen -*.png binary -*.jpg binary - - -# Shell-Skripte: immer LF -*.sh text eol=lf - -# Windows-Batch-Dateien: immer CRLF -*.bat text eol=crlf -*.cmd text eol=crlf - -# Python, JS, HTML etc.: bevorzugt LF (aber nicht erzwungen) -*.py text eol=lf -*.js text eol=lf -*.ts text eol=lf -*.html text eol=lf -*.css text eol=lf -*.json text eol=lf -*.yml text eol=lf -*.yaml text eol=lf -*.md text eol=lf - -# Konfigurationsdateien -*.env text eol=lf -*.conf text eol=lf -*.ini text eol=lf - -# Binärdateien -*.png binary -*.jpg binary -*.jpeg binary -*.gif binary -*.webp binary -*.pdf binary -*.ico binary -*.ttf binary -*.woff binary -*.zip binary -*.tar binary -*.gz binary -*.7z binary -*.exe binary -*.dll binary diff --git a/.gitignore b/.gitignore index e17ef61b..c61c2928 100644 --- a/.gitignore +++ b/.gitignore @@ -1,87 +1,398 @@ -# MYP - Manage your Printer .gitignore +# 📦 MYP - Manage your Printer .gitignore +# Umfassende Git-Ignore-Konfiguration für Microservice-Architektur -# Betriebssystem-spezifische Dateien -.DS_Store -Thumbs.db -desktop.ini +# ======================================================================================== +# 🏗️ INFRASTRUKTUR UND CONTAINER +# ======================================================================================== -# Sensible Daten und Konfigurationen +# Docker +.dockerignore +docker-compose.override.yml +**/.docker/ +**/Dockerfile.local +**/*.dockerfile.local + +# Container-Volumes und -Daten +volumes/ +data/ +**/instance/ +**/logs/ +caddy_data/ +caddy_config/ + +# Monitoring-Daten +monitoring/prometheus/data/ +monitoring/grafana/data/ +monitoring/grafana/logs/ + +# ======================================================================================== +# 🔐 SICHERHEIT UND GEHEIMNISSE +# ======================================================================================== + +# Umgebungsvariablen und Geheimnisse +**/.env +**/.env.* +!**/.env.example +**/*.pem +**/*.key +**/*.cer +**/*.crt +**/*.p12 +**/*.pfx + +# Sichere Konfigurationen config/secure/ -*.env -*.pem -*.key -*.cer -.env.local -.env.development.local -.env.test.local -.env.production.local +infrastructure/ssl/ +**/secrets/ +**/private/ -# Python spezifische Dateien -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg -venv/ -ENV/ +# SSH-Schlüssel +**/.ssh/ +**/id_rsa* +**/id_ed25519* -# JavaScript/Node spezifische Dateien -node_modules/ -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* -.next/ -out/ -.vercel -*.tsbuildinfo +# ======================================================================================== +# 🐍 PYTHON/FLASK BACKEND +# ======================================================================================== -# IDE Dateien -.idea/ +# Python-Bytecode +**/__pycache__/ +**/*.py[cod] +**/*$py.class +**/*.so + +# Verteilung / Paketierung +backend/build/ +backend/develop-eggs/ +backend/dist/ +backend/downloads/ +backend/eggs/ +backend/.eggs/ +backend/lib/ +backend/lib64/ +backend/parts/ +backend/sdist/ +backend/var/ +backend/wheels/ +backend/share/python-wheels/ +backend/*.egg-info/ +backend/.installed.cfg +backend/*.egg +backend/MANIFEST + +# PyInstaller +backend/*.manifest +backend/*.spec + +# Unit-Test / Coverage-Berichte +backend/htmlcov/ +backend/.tox/ +backend/.nox/ +backend/.coverage +backend/.coverage.* +backend/.cache +backend/nosetests.xml +backend/coverage.xml +backend/*.cover +backend/*.py,cover +backend/.hypothesis/ +backend/.pytest_cache/ +backend/cover/ + +# Jupyter Notebook +backend/.ipynb_checkpoints + +# IPython +backend/profile_default/ +backend/ipython_config.py + +# Umgebungen +backend/.env +backend/.venv +backend/env/ +backend/venv/ +backend/ENV/ +backend/env.bak/ +backend/venv.bak/ + +# Spyder-Projekt-Einstellungen +backend/.spyderproject +backend/.spyproject + +# Rope-Projekt-Einstellungen +backend/.ropeproject + +# mkdocs-Dokumentation +backend/site + +# mypy +backend/.mypy_cache/ +backend/.dmypy.json +backend/dmypy.json + +# Pyre Type Checker +backend/.pyre/ + +# pytype Static Type Analyzer +backend/.pytype/ + +# Cython Debug-Symbole +backend/cython_debug/ + +# Spezifische Backend-Dateien +backend/instance/ +backend/logs/ +backend/*.db +backend/*.sqlite +backend/*.sqlite3 + +# ======================================================================================== +# 📱 NODE.JS/NEXT.JS FRONTEND +# ======================================================================================== + +# Abhängigkeiten +frontend/node_modules/ +frontend/.pnp +frontend/.pnp.js + +# Testing +frontend/coverage/ + +# Next.js +frontend/.next/ +frontend/out/ + +# Produktions-Build +frontend/build + +# Verschiedenes +frontend/.DS_Store +frontend/*.tsbuildinfo +frontend/next-env.d.ts + +# Debug-Logs +frontend/npm-debug.log* +frontend/yarn-debug.log* +frontend/yarn-error.log* +frontend/.pnpm-debug.log* + +# Lokale Umgebungsdateien +frontend/.env +frontend/.env.local +frontend/.env.development.local +frontend/.env.test.local +frontend/.env.production.local + +# Vercel +frontend/.vercel + +# TypeScript +frontend/*.tsbuildinfo + +# Storybook-Build-Ausgaben +frontend/storybook-static + +# Datenbank +frontend/db/ +frontend/*.db +frontend/*.sqlite + +# ======================================================================================== +# 💻 ENTWICKLUNGSUMGEBUNG UND IDE +# ======================================================================================== + +# Visual Studio Code .vscode/ +*.code-workspace + +# JetBrains IDEs +.idea/ +*.iws +*.iml +*.ipr + +# Sublime Text +*.sublime-project +*.sublime-workspace + +# Vim *.swp *.swo *~ -.project -.classpath -.settings/ +.vimrc.local -# Logs und temporäre Dateien -logs/ -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -temp/ +# Emacs +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Eclipse +.metadata +bin/ tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders -# Datenbanken und SQLite Dateien -*.sqlite -*.sqlite3 -*.db -instance/ +# NetBeans +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ -# Kompilierte Dateien und Binaries -*.com -*.class -*.dll -*.exe -*.o -*.a -*.so -*.dylib \ No newline at end of file +# ======================================================================================== +# 🖥️ BETRIEBSSYSTEM +# ======================================================================================== + +# Windows +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db +*.stackdump +[Dd]esktop.ini +$RECYCLE.BIN/ +*.cab +*.msi +*.msix +*.msm +*.msp +*.lnk + +# macOS +.DS_Store +.AppleDouble +.LSOverride +Icon +._* +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Linux +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* + +# ======================================================================================== +# 📊 MONITORING UND LOGGING +# ======================================================================================== + +# Log-Dateien +*.log +logs/ +**/*.log +**/*.log.* + +# Monitoring-Daten +prometheus/data/ +grafana/data/ +grafana/logs/ + +# Backup-Dateien +*.backup +*.bak +backups/ + +# ======================================================================================== +# 🧪 TESTING UND QUALITÄTSSICHERUNG +# ======================================================================================== + +# Test-Ergebnisse +test-results/ +test-reports/ +coverage/ +.nyc_output/ + +# Jest +**/.jest/ + +# Cypress +**/cypress/videos/ +**/cypress/screenshots/ + +# Playwright +test-results/ +playwright-report/ +playwright/.cache/ + +# ======================================================================================== +# 📦 PAKETIERUNG UND VERTEILUNG +# ======================================================================================== + +# Tar-Archive +*.tar +*.tar.gz +*.tar.bz2 +*.tar.xz + +# Komprimierte Dateien +*.zip +*.rar +*.7z + +# Build-Artefakte +dist/ +build/ +out/ + +# ======================================================================================== +# 🔄 TEMPORÄRE UND CACHE-DATEIEN +# ======================================================================================== + +# Allgemeine temporäre Dateien +tmp/ +temp/ +.tmp/ +.temp/ + +# Cache-Verzeichnisse +.cache/ +**/.cache/ +.eslintcache +.parcel-cache/ + +# Lock-Dateien (falls gewünscht - auskommentieren) +# package-lock.json +# yarn.lock +# pnpm-lock.yaml + +# ======================================================================================== +# 🏭 PRODUKTIONSSPEZIFISCHE DATEIEN +# ======================================================================================== + +# Produktions-Konfigurationen +docker-compose.prod.yml +docker-compose.production.yml +production.env + +# SSL-Zertifikate für Produktion +ssl/ +certificates/ +certs/ + +# Backup-Skripte und -Daten +backup/ +snapshots/ \ No newline at end of file diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/PROJECT_STRUCTURE.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/backend/app.py b/backend/app.py index 4ea3fb5e..63ace241 100755 --- a/backend/app.py +++ b/backend/app.py @@ -1,5 +1,4 @@ -from flask import Flask, request, jsonify, g, redirect, url_for, session as flask_session, render_template, flash -from flask_cors import CORS +from flask import Flask, request, jsonify, g, redirect, url_for, session as flask_session, render_template, flash, send_from_directory from werkzeug.security import generate_password_hash, check_password_hash import secrets # Für bessere Salt-Generierung from functools import wraps @@ -17,20 +16,82 @@ from datetime import timedelta from PyP100 import PyP100 from dotenv import load_dotenv +# Importiere Konfiguration +from config import config + # Importiere Netzwerkkonfiguration from network_config import NetworkConfig +# Importiere Frontend V2 Blueprint +from frontend_v2_routes import frontend_v2, set_app_functions + # Lade Umgebungsvariablen load_dotenv() -# Initialisierung +def create_app(config_name=None): + """ + Application Factory Pattern für die Flask-Anwendung. + + Args: + config_name: Name der zu verwendenden Konfiguration ('development', 'production', 'testing') + + Returns: + Flask: Konfigurierte Flask-Anwendung + """ + app = Flask(__name__) + + # Bestimme Konfiguration + if config_name is None: + config_name = os.environ.get('FLASK_ENV', 'development') + + # Lade Konfiguration + config_object = config.get(config_name, config['default']) + app.config.from_object(config_object) + + # Initialisiere Konfiguration + config_object.init_app(app) + + # Initialisiere Netzwerkkonfiguration + network_config = NetworkConfig(app) + + # Registriere Blueprint + app.register_blueprint(frontend_v2, url_prefix='/frontend_v2') + + # Konfiguriere statische Dateien für Frontend v2 + @app.route('/frontend_v2/static/') + def frontend_v2_static(filename): + return send_from_directory(os.path.join(app.root_path, 'frontend_v2/static'), filename) + + # Globale Variablen + app.config['PRINTERS'] = json.loads(app.config.get('PRINTERS', '{}')) + + # Database functions registrieren + register_database_functions(app) + + # Authentifizierung registrieren + register_auth_functions(app) + + # API-Routen registrieren + register_api_routes(app) + + # Web-UI-Routen registrieren + register_web_routes(app) + + # Error-Handler registrieren + register_error_handlers(app) + + # Hintergrund-Tasks registrieren + register_background_tasks(app) + + return app + +# Initialisierung - wird später durch create_app ersetzt app = Flask(__name__) -CORS(app, supports_credentials=True) # Initialisiere Netzwerkkonfiguration network_config = NetworkConfig(app) -# Konfiguration +# Temporäre Konfiguration für Legacy-Code app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev_secret_key') app.config['DATABASE'] = os.environ.get('DATABASE_PATH', 'instance/myp.db') app.config['SESSION_COOKIE_HTTPONLY'] = True @@ -39,11 +100,65 @@ app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7) app.config['JOB_CHECK_INTERVAL'] = int(os.environ.get('JOB_CHECK_INTERVAL', '60')) # Sekunden +# Registriere Frontend V2 Blueprint +app.register_blueprint(frontend_v2, url_prefix='/frontend_v2') + +# Übergebe Funktionen an das Frontend v2 +def setup_frontend_v2(): + app_functions = { + 'get_current_user': get_current_user, + 'get_user_by_id': get_user_by_id, + 'get_socket_by_id': get_socket_by_id, + 'get_job_by_id': get_job_by_id, + 'get_all_sockets': get_all_sockets, + 'get_all_users': get_all_users, + 'get_all_jobs': get_all_jobs, + 'get_jobs_by_user': get_jobs_by_user, + 'login_required': login_required, + 'admin_required': admin_required, + 'delete_session': delete_session, + 'socket_to_dict': socket_to_dict, + 'job_to_dict': job_to_dict, + 'user_to_dict': user_to_dict + } + set_app_functions(app_functions) + +# Konfiguriere statische Dateien für Frontend v2 +@app.route('/frontend_v2/static/') +def frontend_v2_static(filename): + return send_from_directory(os.path.join(app.root_path, 'frontend_v2/static'), filename) + # Steckdosen-Konfiguration TAPO_USERNAME = os.environ.get('TAPO_USERNAME') TAPO_PASSWORD = os.environ.get('TAPO_PASSWORD') -# Logging +def setup_logging(app): + """ + Konfiguriert das Logging basierend auf der Umgebung. + + Args: + app: Flask-Anwendung + """ + if not app.debug and not app.testing: + # Production logging + if not os.path.exists('logs'): + os.mkdir('logs') + + file_handler = RotatingFileHandler('logs/myp.log', maxBytes=10240, backupCount=10) + file_handler.setFormatter(logging.Formatter( + '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]' + )) + file_handler.setLevel(logging.INFO) + app.logger.addHandler(file_handler) + + app.logger.setLevel(logging.INFO) + app.logger.info('MYP Backend starting in production mode') + else: + # Development logging + app.logger.setLevel(logging.DEBUG) + app.logger.info('MYP Backend starting in development mode') + +# Logging - Legacy (wird durch setup_logging ersetzt) if not os.path.exists('logs'): os.mkdir('logs') file_handler = RotatingFileHandler('logs/myp.log', maxBytes=10240, backupCount=10) @@ -1686,9 +1801,33 @@ def stats_page(): return redirect(url_for('index')) return render_template('stats.html', current_user=current_user, active_page='stats') -# Initialisierung und Start des Hintergrund-Threads beim ersten Request -with app.app_context(): - # Diese Funktion wird nach dem App-Start aber vor dem ersten Request ausgeführt +# Registrierungsfunktionen für modularen Aufbau +def register_database_functions(app): + """Registriert Database-Funktionen und Teardown-Handler.""" + app.teardown_appcontext(close_db) + +def register_auth_functions(app): + """Registriert Authentifizierungsfunktionen.""" + # Authentifizierungsfunktionen sind bereits global definiert + pass + +def register_api_routes(app): + """Registriert alle API-Routen.""" + # API-Routen sind bereits global definiert + pass + +def register_web_routes(app): + """Registriert alle Web-UI-Routen.""" + # Web-Routen sind bereits global definiert + pass + +def register_error_handlers(app): + """Registriert Error-Handler.""" + # Error-Handler sind bereits global definiert + pass + +def register_background_tasks(app): + """Registriert Hintergrund-Tasks.""" @app.before_request def initialize_background_tasks(): """Startet den Hintergrund-Thread für Job-Überprüfung beim ersten Request.""" @@ -1711,6 +1850,7 @@ with app.app_context(): # Server starten if __name__ == '__main__': + # Legacy-Modus für direkte Ausführung with app.app_context(): init_db() if PRINTERS: @@ -1721,5 +1861,21 @@ if __name__ == '__main__': job_thread = threading.Thread(target=background_job_checker, daemon=True, name='job_checker_thread') job_thread.start() app.logger.info("Hintergrund-Thread für Job-Überprüfung gestartet") + setup_frontend_v2() - app.run(debug=True, host='0.0.0.0') \ No newline at end of file + # Produktionsmodus aktivieren + flask_env = os.environ.get('FLASK_ENV', 'development') + debug_mode = flask_env == 'development' + + app.run(host='0.0.0.0', port=5000, debug=debug_mode) +else: + # Für WSGI-Server wie Gunicorn - verwende Application Factory + flask_env = os.environ.get('FLASK_ENV', 'production') + app = create_app(flask_env) + + with app.app_context(): + init_db() + printers_config = json.loads(app.config.get('PRINTERS', '{}')) + if printers_config: + init_printers() + setup_frontend_v2() \ No newline at end of file diff --git a/backend/cleanup.sh b/backend/cleanup.sh new file mode 100644 index 00000000..2fa77d5f --- /dev/null +++ b/backend/cleanup.sh @@ -0,0 +1,218 @@ +#!/bin/bash + +# Raspberry Pi Bereinigungsskript für MYP-Projekt +# Dieses Skript bereinigt alte Docker-Installationen und installiert alle erforderlichen Abhängigkeiten + +# Farbcodes für Ausgabe +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Funktion zur Ausgabe mit Zeitstempel +log() { + echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" +} + +error_log() { + echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] FEHLER:${NC} $1" >&2 +} + +# Prüfen, ob das Skript mit Root-Rechten ausgeführt wird +if [ "$EUID" -ne 0 ]; then + error_log "Dieses Skript muss mit Root-Rechten ausgeführt werden (sudo)." + exit 1 +fi + +log "${YELLOW}=== MYP Raspberry Pi Bereinigung und Setup ===${NC}" +log "Diese Skript wird alle alten Docker-Installationen entfernen und die erforderlichen Abhängigkeiten neu installieren." + +# Sicherstellen, dass apt funktioniert +log "Aktualisiere apt-Paketindex..." +apt-get update || { + error_log "Konnte apt-Paketindex nicht aktualisieren." + exit 1 +} + +# Installiere grundlegende Abhängigkeiten +log "Installiere grundlegende Abhängigkeiten..." +apt-get install -y \ + apt-transport-https \ + ca-certificates \ + curl \ + gnupg \ + lsb-release \ + wget \ + git \ + jq \ + || { + error_log "Konnte grundlegende Abhängigkeiten nicht installieren." + exit 1 +} + +# Stoppe alle laufenden Docker-Container +log "${YELLOW}Stoppe alle laufenden Docker-Container...${NC}" +if command -v docker &> /dev/null; then + docker stop $(docker ps -aq) 2>/dev/null || true + log "Alle Docker-Container gestoppt." +else + log "Docker ist nicht installiert, keine Container zu stoppen." +fi + +# Entferne alte Docker-Installation +log "${YELLOW}Entferne alte Docker-Installation...${NC}" +apt-get remove -y docker docker-engine docker.io containerd runc docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-compose || true +apt-get autoremove -y || true +rm -rf /var/lib/docker /var/lib/containerd /var/run/docker.sock /etc/docker /usr/local/bin/docker-compose 2>/dev/null || true +log "${GREEN}Alte Docker-Installation entfernt.${NC}" + +# Entferne alte Projektcontainer und -Dateien +log "${YELLOW}Entferne alte MYP-Projektcontainer und -Dateien...${NC}" +if command -v docker &> /dev/null; then + # Entferne Container + docker rm -f myp-frontend myp-backend 2>/dev/null || true + # Entferne Images + docker rmi -f myp-frontend myp-backend 2>/dev/null || true + # Entferne unbenutzte Volumes und Netzwerke + docker system prune -af --volumes 2>/dev/null || true +fi + +# Erkennen der Raspberry Pi-Architektur +log "Erkenne Systemarchitektur..." +ARCH=$(dpkg --print-architecture) +log "Erkannte Architektur: ${ARCH}" + +# Installiere Docker mit dem offiziellen Convenience-Skript +log "${YELLOW}Installiere Docker mit dem offiziellen Convenience-Skript...${NC}" +curl -fsSL https://get.docker.com -o get-docker.sh +sh get-docker.sh --channel stable + +# Überprüfen, ob Docker erfolgreich installiert wurde +if ! command -v docker &> /dev/null; then + error_log "Docker-Installation fehlgeschlagen!" + exit 1 +fi + +log "${GREEN}Docker erfolgreich installiert!${NC}" + +# Füge den aktuellen Benutzer zur Docker-Gruppe hinzu +if [ "$SUDO_USER" ]; then + log "Füge Benutzer $SUDO_USER zur Docker-Gruppe hinzu..." + usermod -aG docker $SUDO_USER + log "${YELLOW}Hinweis: Eine Neuanmeldung ist erforderlich, damit die Gruppenänderung wirksam wird.${NC}" +fi + +# Konfiguriere Docker mit DNS-Servern für bessere Netzwerkkompatibilität +log "Konfiguriere Docker mit Google DNS..." +mkdir -p /etc/docker +cat > /etc/docker/daemon.json << EOL +{ + "dns": ["8.8.8.8", "8.8.4.4"] +} +EOL + +# Starte Docker-Dienst neu +log "Starte Docker-Dienst neu..." +systemctl restart docker +systemctl enable docker +log "${GREEN}Docker-Dienst neu gestartet und für den Autostart aktiviert.${NC}" + +# Installiere Docker Compose v2 +log "${YELLOW}Installiere Docker Compose...${NC}" + +# Bestimme die passende Docker Compose-Version für die Architektur +if [ "$ARCH" = "armhf" ]; then + log "Installiere Docker Compose für armhf (32-bit)..." + curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-armv7" -o /usr/local/bin/docker-compose +elif [ "$ARCH" = "arm64" ]; then + log "Installiere Docker Compose für arm64 (64-bit)..." + curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-aarch64" -o /usr/local/bin/docker-compose +else + log "Unbekannte Architektur, verwende automatische Erkennung..." + curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +fi + +chmod +x /usr/local/bin/docker-compose + +# Überprüfe, ob Docker Compose installiert wurde +if ! command -v docker-compose &> /dev/null; then + error_log "Docker Compose-Installation fehlgeschlagen!" + exit 1 +fi + +# Installiere Docker Compose Plugin (neuere Methode) +log "Installiere Docker Compose Plugin..." +apt-get update +apt-get install -y docker-compose-plugin + +log "${GREEN}Docker Compose erfolgreich installiert!${NC}" +docker compose version || docker-compose --version + +# Installiere zusätzliche Abhängigkeiten für die Projektunterstützung +log "${YELLOW}Installiere zusätzliche Projektabhängigkeiten...${NC}" +apt-get install -y \ + python3 \ + python3-pip \ + sqlite3 \ + build-essential \ + libffi-dev \ + libssl-dev \ + || { + error_log "Konnte zusätzliche Abhängigkeiten nicht installieren." + exit 1 +} + +# Optimieren des Raspberry Pi für Docker-Workloads +log "${YELLOW}Optimiere Raspberry Pi für Docker-Workloads...${NC}" + +# Swap erhöhen für bessere Performance bei begrenztem RAM +log "Konfiguriere Swap-Größe..." +CURRENT_SWAP=$(grep "CONF_SWAPSIZE" /etc/dphys-swapfile | cut -d= -f2) +log "Aktuelle Swap-Größe: ${CURRENT_SWAP}" + +# Erhöhe Swap auf 2GB, wenn weniger +if [ "$CURRENT_SWAP" -lt 2048 ]; then + sed -i 's/^CONF_SWAPSIZE=.*/CONF_SWAPSIZE=2048/' /etc/dphys-swapfile + log "Swap-Größe auf 2048MB erhöht, Neustart des Swap-Dienstes erforderlich." + + # Neustart des Swap-Dienstes + /etc/init.d/dphys-swapfile restart +else + log "Swap-Größe ist bereits ausreichend." +fi + +# Konfiguriere cgroup für Docker +if ! grep -q "cgroup_enable=memory" /boot/cmdline.txt; then + log "Konfiguriere cgroup für Docker..." + CMDLINE=$(cat /boot/cmdline.txt) + echo "$CMDLINE cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1" > /boot/cmdline.txt + log "${YELLOW}WICHTIG: Ein Systemneustart ist erforderlich, damit die cgroup-Änderungen wirksam werden.${NC}" +fi + +# Prüfe, ob Backend-Installationsdateien vorhanden sind +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +BACKEND_DIR="$SCRIPT_DIR" + +if [ -d "$BACKEND_DIR" ] && [ -f "$BACKEND_DIR/docker-compose.yml" ]; then + log "${GREEN}Backend-Projektdateien gefunden in $BACKEND_DIR${NC}" +else + log "${YELLOW}Warnung: Backend-Projektdateien nicht gefunden in $BACKEND_DIR${NC}" +fi + +# Abschlussmeldung +log "${GREEN}=== Bereinigung und Setup abgeschlossen ===${NC}" +log "${YELLOW}WICHTIGE HINWEISE:${NC}" +log "1. Ein ${RED}SYSTEMNEUSTART${NC} ist ${RED}DRINGEND ERFORDERLICH${NC}, damit alle Änderungen wirksam werden." +log "2. Nach dem Neustart können Sie das Backend-Installationsskript ausführen:" +log " cd $BACKEND_DIR && ./install.sh" +log "3. Bei Problemen mit Docker-Berechtigungen stellen Sie sicher, dass Sie sich neu angemeldet haben." + +echo "" +read -p "Möchten Sie das System jetzt neu starten? (j/n): " REBOOT_CHOICE +if [[ "$REBOOT_CHOICE" == "j" ]]; then + log "System wird neu gestartet..." + reboot +else + log "Bitte starten Sie das System manuell neu, bevor Sie die Installationsskripte ausführen." +fi \ No newline at end of file diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 00000000..9dc97f6f --- /dev/null +++ b/backend/config.py @@ -0,0 +1,145 @@ +""" +Konfigurationsklassen für die MYP Flask-Anwendung. +Definiert verschiedene Konfigurationen für Development, Production und Testing. +""" + +import os +from datetime import timedelta +import secrets + +class Config: + """Basis-Konfigurationsklasse mit gemeinsamen Einstellungen.""" + + SECRET_KEY = os.environ.get('SECRET_KEY') or secrets.token_hex(32) + DATABASE = os.environ.get('DATABASE_PATH', 'instance/myp.db') + + # Session-Konfiguration + SESSION_COOKIE_HTTPONLY = True + SESSION_COOKIE_SAMESITE = 'Lax' + PERMANENT_SESSION_LIFETIME = timedelta(days=7) + + # Job-Konfiguration + JOB_CHECK_INTERVAL = int(os.environ.get('JOB_CHECK_INTERVAL', '60')) # Sekunden + + # Tapo-Konfiguration + TAPO_USERNAME = os.environ.get('TAPO_USERNAME') + TAPO_PASSWORD = os.environ.get('TAPO_PASSWORD') + + # Logging-Konfiguration + LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO') + LOG_MAX_BYTES = int(os.environ.get('LOG_MAX_BYTES', '10485760')) # 10MB + LOG_BACKUP_COUNT = int(os.environ.get('LOG_BACKUP_COUNT', '10')) + + # Drucker-Konfiguration + PRINTERS = os.environ.get('PRINTERS', '{}') + + @staticmethod + def init_app(app): + """Initialisierung der Anwendung mit der Konfiguration.""" + pass + +class DevelopmentConfig(Config): + """Konfiguration für die Entwicklungsumgebung.""" + + DEBUG = True + TESTING = False + + # Session-Cookies in Development weniger strikt + SESSION_COOKIE_SECURE = False + + # Kürzere Job-Check-Intervalle für schnellere Entwicklung + JOB_CHECK_INTERVAL = int(os.environ.get('JOB_CHECK_INTERVAL', '30')) + + @staticmethod + def init_app(app): + Config.init_app(app) + + # Development-spezifische Initialisierung + import logging + logging.basicConfig(level=logging.DEBUG) + +class ProductionConfig(Config): + """Konfiguration für die Produktionsumgebung.""" + + DEBUG = False + TESTING = False + + # Sichere Session-Cookies in Production + SESSION_COOKIE_SECURE = True + SESSION_COOKIE_HTTPONLY = True + SESSION_COOKIE_SAMESITE = 'Strict' + + # Strengere Sicherheitseinstellungen + WTF_CSRF_ENABLED = True + WTF_CSRF_TIME_LIMIT = None + + # Längere Job-Check-Intervalle für bessere Performance + JOB_CHECK_INTERVAL = int(os.environ.get('JOB_CHECK_INTERVAL', '60')) + + @staticmethod + def init_app(app): + Config.init_app(app) + + # Production-spezifische Initialisierung + import logging + from logging.handlers import RotatingFileHandler, SysLogHandler + + # Datei-Logging + if not os.path.exists('logs'): + os.mkdir('logs') + + file_handler = RotatingFileHandler( + 'logs/myp.log', + maxBytes=Config.LOG_MAX_BYTES, + backupCount=Config.LOG_BACKUP_COUNT + ) + file_handler.setFormatter(logging.Formatter( + '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]' + )) + file_handler.setLevel(logging.INFO) + app.logger.addHandler(file_handler) + + # Error-Logging + error_handler = RotatingFileHandler( + 'logs/myp-errors.log', + maxBytes=Config.LOG_MAX_BYTES, + backupCount=Config.LOG_BACKUP_COUNT + ) + error_handler.setFormatter(logging.Formatter( + '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]' + )) + error_handler.setLevel(logging.ERROR) + app.logger.addHandler(error_handler) + + app.logger.setLevel(logging.INFO) + app.logger.info('MYP Backend starting in production mode') + +class TestingConfig(Config): + """Konfiguration für die Testumgebung.""" + + DEBUG = True + TESTING = True + + # In-Memory-Datenbank für Tests + DATABASE = ':memory:' + + # Deaktiviere CSRF für Tests + WTF_CSRF_ENABLED = False + + # Kürzere Session-Lebensdauer für Tests + PERMANENT_SESSION_LIFETIME = timedelta(minutes=5) + + # Kürzere Job-Check-Intervalle für Tests + JOB_CHECK_INTERVAL = 5 + + @staticmethod + def init_app(app): + Config.init_app(app) + +# Konfigurationsmapping +config = { + 'development': DevelopmentConfig, + 'production': ProductionConfig, + 'testing': TestingConfig, + 'default': DevelopmentConfig +} \ No newline at end of file diff --git a/backend/debug-server/static/css/debug-dashboard.css b/backend/debug-server/static/css/debug-dashboard.css new file mode 100644 index 00000000..dbbc98f4 --- /dev/null +++ b/backend/debug-server/static/css/debug-dashboard.css @@ -0,0 +1,617 @@ +/* Debug-Dashboard CSS */ + +/* Reset und Basis-Stile */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + line-height: 1.6; + color: #333; + background-color: #f5f7fa; + padding: 0; + margin: 0; +} + +h1, h2, h3, h4 { + color: #2c3e50; + margin-bottom: 15px; +} + +/* Layout */ +.page-header { + background-color: #2c3e50; + color: white; + padding: 20px; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; +} + +.page-header h1 { + color: white; + margin: 0; +} + +.last-update { + font-size: 0.9em; + opacity: 0.8; +} + +.header-actions { + display: flex; + gap: 10px; +} + +.dashboard-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); + gap: 20px; + padding: 20px; +} + +.dashboard-section { + padding: 20px; +} + +/* Karten */ +.card { + background-color: white; + border-radius: 5px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + overflow: hidden; + margin-bottom: 20px; +} + +.card-header { + background-color: #f1f5f9; + padding: 15px; + font-weight: bold; + border-bottom: 1px solid #e2e8f0; + display: flex; + justify-content: space-between; + align-items: center; +} + +.card-body { + padding: 20px; +} + +/* Statistik-Karten */ +.stats-row { + display: flex; + justify-content: space-between; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.stat-card { + background-color: #f8fafc; + border-radius: 5px; + padding: 15px; + min-width: 150px; + text-align: center; + flex: 1; + margin: 0 5px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.stat-label { + font-size: 0.9em; + color: #64748b; + margin-bottom: 5px; +} + +.stat-value { + font-size: 1.5em; + font-weight: bold; + color: #334155; +} + +/* Charts */ +.chart-container { + position: relative; + height: 200px; + margin-bottom: 20px; +} + +.chart-container.small { + height: 150px; +} + +/* Formularelemente */ +.form-group { + margin-bottom: 15px; +} + +.form-group label { + display: block; + margin-bottom: 5px; + font-weight: 500; +} + +.form-group input, +.form-group select { + width: 100%; + padding: 8px 12px; + border: 1px solid #cbd5e1; + border-radius: 4px; + font-size: 1em; +} + +.input-group { + display: flex; +} + +.input-group input { + flex: 1; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group-append { + display: flex; +} + +.input-group-append .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +/* Buttons */ +.btn { + background-color: #3b82f6; + color: white; + border: none; + padding: 8px 15px; + border-radius: 4px; + cursor: pointer; + font-weight: 500; + transition: background-color 0.2s; +} + +.btn:hover { + background-color: #2563eb; +} + +.btn-sm { + padding: 5px 10px; + font-size: 0.9em; +} + +.btn-success { + background-color: #10b981; +} + +.btn-success:hover { + background-color: #059669; +} + +.btn-warning { + background-color: #f59e0b; +} + +.btn-warning:hover { + background-color: #d97706; +} + +.btn-danger { + background-color: #ef4444; +} + +.btn-danger:hover { + background-color: #dc2626; +} + +.btn-group { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +/* Status-Anzeigen */ +.status { + padding: 10px; + border-radius: 4px; + margin-bottom: 10px; + background-color: #f1f5f9; +} + +.status-good { + background-color: #d1fae5; + color: #064e3b; +} + +.status-warning { + background-color: #fff7ed; + color: #7c2d12; +} + +.status-error { + background-color: #fee2e2; + color: #7f1d1d; +} + +/* Nachrichten */ +.message { + display: none; + padding: 15px; + margin: 15px 20px; + border-radius: 5px; + font-weight: 500; +} + +.message-success { + background-color: #d1fae5; + color: #064e3b; +} + +.message-error { + background-color: #fee2e2; + color: #7f1d1d; +} + +/* Systemstatus-Banner */ +.system-health-banner { + display: none; + align-items: center; + padding: 10px 20px; + color: white; + position: relative; +} + +.system-health-banner.checking { + background-color: #3b82f6; +} + +.system-health-banner.healthy { + background-color: #10b981; +} + +.system-health-banner.warning { + background-color: #f59e0b; +} + +.system-health-banner.critical { + background-color: #ef4444; +} + +.health-icon { + font-size: 1.5em; + margin-right: 15px; +} + +.health-status { + flex: 1; +} + +.health-status-title { + font-weight: bold; + margin-bottom: 5px; +} + +.health-details { + font-size: 0.9em; + display: flex; + flex-wrap: wrap; + gap: 15px; +} + +.health-good { + color: #10b981; +} + +.health-warning { + color: #f59e0b; +} + +.health-critical { + color: #ef4444; +} + +/* Tabellen */ +table { + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; +} + +table th, +table td { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid #e2e8f0; +} + +table th { + background-color: #f8fafc; + font-weight: 600; +} + +table tbody tr:hover { + background-color: #f1f5f9; +} + +/* Tabs */ +.tabs { + display: flex; + border-bottom: 1px solid #e2e8f0; + margin-bottom: 20px; +} + +.tab { + padding: 10px 20px; + cursor: pointer; + border-bottom: 2px solid transparent; + transition: all 0.2s; +} + +.tab:hover { + background-color: #f1f5f9; +} + +.tab.active { + border-bottom-color: #3b82f6; + color: #3b82f6; + font-weight: 500; +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +/* Docker-Container */ +.container-row { + transition: background-color 0.2s; +} + +.container-row.running { + border-left: 3px solid #10b981; +} + +.container-row.exited { + border-left: 3px solid #ef4444; +} + +.container-name { + font-weight: bold; +} + +.container-image { + font-size: 0.9em; + color: #64748b; +} + +.container-running { + color: #064e3b; + font-weight: 500; +} + +.filter-bar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +/* Logs und Terminal-Ausgabe */ +.logs-container { + background-color: #1e293b; + color: #e2e8f0; + border-radius: 5px; + padding: 15px; + margin-top: 15px; + max-height: 400px; + overflow-y: auto; + font-family: 'Consolas', 'Monaco', monospace; + line-height: 1.5; +} + +.log-placeholder { + color: #94a3b8; + text-align: center; + padding: 20px; +} + +.log-header { + display: flex; + justify-content: space-between; + padding: 10px; + background-color: #334155; + margin: -15px -15px 10px -15px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} + +.log-line { + white-space: pre-wrap; + word-break: break-all; + margin-bottom: 2px; + padding: 2px 0; +} + +.log-error { + color: #f87171; +} + +.log-warning { + color: #fbbf24; +} + +.log-info { + color: #60a5fa; +} + +.log-debug { + color: #a3e635; +} + +.log-timestamp { + color: #94a3b8; +} + +pre.ping-output, +pre.traceroute-output, +pre.dns-output { + background-color: #1e293b; + color: #e2e8f0; + padding: 15px; + border-radius: 5px; + overflow-x: auto; + white-space: pre-wrap; + font-family: 'Consolas', 'Monaco', monospace; +} + +/* Netzwerkschnittstellen */ +.interface-item { + background-color: #f8fafc; + border-radius: 5px; + padding: 15px; + margin-bottom: 15px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.interface-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + padding-bottom: 10px; + border-bottom: 1px solid #e2e8f0; +} + +.interface-mac { + font-family: monospace; + color: #64748b; +} + +.interface-ips h4, +.interface-stats h4 { + color: #475569; + font-size: 1em; + margin-top: 15px; + margin-bottom: 8px; +} + +.ip-item { + padding: 8px; + background-color: #f1f5f9; + border-radius: 4px; + margin-bottom: 8px; +} + +/* Ergebnisse-Container */ +.results-container { + margin-top: 15px; + max-height: 300px; + overflow-y: auto; +} + +/* Loading-Anzeige */ +.loading { + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + color: #64748b; +} + +.loading::before { + content: ""; + width: 20px; + height: 20px; + margin-right: 10px; + border: 2px solid #cbd5e1; + border-top-color: #3b82f6; + border-radius: 50%; + animation: spinner 0.8s linear infinite; +} + +@keyframes spinner { + to { + transform: rotate(360deg); + } +} + +/* Error-Anzeige */ +.error { + color: #ef4444; + padding: 10px; + border-radius: 4px; + background-color: #fee2e2; +} + +/* Log-Analyse */ +.error-list, +.warning-list { + margin-bottom: 20px; +} + +.error-item, +.warning-item { + background-color: #fee2e2; + border-left: 3px solid #ef4444; + padding: 10px; + margin-bottom: 10px; + border-radius: 4px; +} + +.warning-item { + background-color: #fff7ed; + border-left-color: #f59e0b; +} + +.error-time, +.warning-time { + font-size: 0.9em; + color: #64748b; + margin-bottom: 5px; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .dashboard-container { + grid-template-columns: 1fr; + } + + .page-header { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } + + .header-actions { + width: 100%; + } + + .stats-row { + flex-direction: column; + gap: 10px; + } + + .stat-card { + margin: 0 0 10px 0; + } + + .btn-group { + flex-direction: column; + } + + .tabs { + flex-wrap: wrap; + } + + .tab { + flex: 1; + text-align: center; + padding: 10px; + } +} \ No newline at end of file diff --git a/backend/debug-server/static/js/debug-charts.js b/backend/debug-server/static/js/debug-charts.js new file mode 100644 index 00000000..b22604a9 --- /dev/null +++ b/backend/debug-server/static/js/debug-charts.js @@ -0,0 +1,470 @@ +/* + * Debug-Charts.js + * JavaScript-Funktionen für das Rendering von Diagrammen und Visualisierungen + * im MYP Debug-Server. + */ + +// Globale Variablen für Charts +let cpuUsageChart = null; +let memoryUsageChart = null; +let networkTrafficChart = null; +let diskUsageChart = null; +let containerStatusChart = null; + +// Hilfsfunktion zum Formatieren von Bytes +function formatBytes(bytes, decimals = 2) { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} + +// Aktualisiere Systemdiagramme +function updateSystemCharts() { + fetch('/api/system/metrics') + .then(response => response.json()) + .then(data => { + // CPU-Nutzung aktualisieren + if (cpuUsageChart) { + cpuUsageChart.data.labels.push(new Date().toLocaleTimeString()); + cpuUsageChart.data.datasets[0].data.push(data.cpu_percent); + + // Behalte nur die letzten 30 Datenpunkte + if (cpuUsageChart.data.labels.length > 30) { + cpuUsageChart.data.labels.shift(); + cpuUsageChart.data.datasets[0].data.shift(); + } + + cpuUsageChart.update(); + } + + // Speichernutzung aktualisieren + if (memoryUsageChart) { + memoryUsageChart.data.datasets[0].data = [ + data.memory.used, + data.memory.available + ]; + memoryUsageChart.update(); + + // Aktualisiere die Speicherinfo-Texte + document.getElementById('memory-used').textContent = formatBytes(data.memory.used); + document.getElementById('memory-available').textContent = formatBytes(data.memory.available); + document.getElementById('memory-total').textContent = formatBytes(data.memory.total); + } + + // Festplattennutzung aktualisieren + if (diskUsageChart && data.disk_usage) { + const diskLabels = []; + const diskUsed = []; + const diskFree = []; + + for (const disk of data.disk_usage) { + diskLabels.push(disk.mountpoint); + diskUsed.push(disk.used); + diskFree.push(disk.free); + } + + diskUsageChart.data.labels = diskLabels; + diskUsageChart.data.datasets[0].data = diskUsed; + diskUsageChart.data.datasets[1].data = diskFree; + diskUsageChart.update(); + } + }) + .catch(error => console.error('Fehler beim Abrufen der Systemmetriken:', error)); +} + +// Initialisiere CPU-Nutzungsdiagramm +function initCpuUsageChart() { + const ctx = document.getElementById('cpu-usage-chart').getContext('2d'); + + cpuUsageChart = new Chart(ctx, { + type: 'line', + data: { + labels: [], + datasets: [{ + label: 'CPU-Auslastung (%)', + data: [], + borderColor: 'rgb(75, 192, 192)', + tension: 0.1, + fill: true, + backgroundColor: 'rgba(75, 192, 192, 0.2)' + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { + beginAtZero: true, + max: 100, + title: { + display: true, + text: 'Auslastung (%)' + } + }, + x: { + title: { + display: true, + text: 'Zeit' + } + } + }, + plugins: { + title: { + display: true, + text: 'CPU-Auslastung', + font: { + size: 16 + } + } + } + } + }); +} + +// Initialisiere Speichernutzungsdiagramm +function initMemoryUsageChart() { + const ctx = document.getElementById('memory-usage-chart').getContext('2d'); + + memoryUsageChart = new Chart(ctx, { + type: 'doughnut', + data: { + labels: ['Verwendet', 'Verfügbar'], + datasets: [{ + data: [0, 0], + backgroundColor: [ + 'rgba(255, 99, 132, 0.7)', + 'rgba(75, 192, 192, 0.7)' + ] + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + title: { + display: true, + text: 'Speichernutzung', + font: { + size: 16 + } + } + } + } + }); +} + +// Initialisiere Festplattennutzungsdiagramm +function initDiskUsageChart() { + const ctx = document.getElementById('disk-usage-chart').getContext('2d'); + + diskUsageChart = new Chart(ctx, { + type: 'bar', + data: { + labels: [], + datasets: [ + { + label: 'Belegt', + data: [], + backgroundColor: 'rgba(255, 99, 132, 0.7)' + }, + { + label: 'Frei', + data: [], + backgroundColor: 'rgba(75, 192, 192, 0.7)' + } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + stacked: true, + title: { + display: true, + text: 'Laufwerk' + } + }, + y: { + stacked: true, + title: { + display: true, + text: 'Speicherplatz (Bytes)' + }, + ticks: { + callback: function(value) { + return formatBytes(value); + } + } + } + }, + plugins: { + title: { + display: true, + text: 'Festplattennutzung', + font: { + size: 16 + } + } + } + } + }); +} + +// Aktualisiere Docker-Container-Status +function updateContainerStatus() { + fetch('/api/docker/status') + .then(response => response.json()) + .then(data => { + const containerTable = document.getElementById('container-table'); + if (!containerTable) return; + + // Tabelle leeren + containerTable.innerHTML = ''; + + // Überschriftenzeile + const headerRow = document.createElement('tr'); + ['Name', 'Status', 'CPU', 'Speicher', 'Netzwerk', 'Aktionen'].forEach(header => { + const th = document.createElement('th'); + th.textContent = header; + headerRow.appendChild(th); + }); + containerTable.appendChild(headerRow); + + // Containerdaten + data.containers.forEach(container => { + const row = document.createElement('tr'); + + // Name + const nameCell = document.createElement('td'); + nameCell.textContent = container.name; + row.appendChild(nameCell); + + // Status + const statusCell = document.createElement('td'); + const statusBadge = document.createElement('span'); + statusBadge.textContent = container.status; + statusBadge.className = container.running ? 'status-badge running' : 'status-badge stopped'; + statusCell.appendChild(statusBadge); + row.appendChild(statusCell); + + // CPU + const cpuCell = document.createElement('td'); + cpuCell.textContent = container.cpu_percent ? `${container.cpu_percent.toFixed(2)}%` : 'N/A'; + row.appendChild(cpuCell); + + // Speicher + const memoryCell = document.createElement('td'); + memoryCell.textContent = container.memory_usage ? formatBytes(container.memory_usage) : 'N/A'; + row.appendChild(memoryCell); + + // Netzwerk + const networkCell = document.createElement('td'); + if (container.network_io) { + networkCell.innerHTML = `↓ ${formatBytes(container.network_io.rx_bytes)}
↑ ${formatBytes(container.network_io.tx_bytes)}`; + } else { + networkCell.textContent = 'N/A'; + } + row.appendChild(networkCell); + + // Aktionen + const actionsCell = document.createElement('td'); + const actionsDiv = document.createElement('div'); + actionsDiv.className = 'container-actions'; + + // Restart-Button + const restartBtn = document.createElement('button'); + restartBtn.className = 'btn btn-warning btn-sm'; + restartBtn.innerHTML = ''; + restartBtn.title = 'Container neustarten'; + restartBtn.onclick = () => restartContainer(container.id); + actionsDiv.appendChild(restartBtn); + + // Logs-Button + const logsBtn = document.createElement('button'); + logsBtn.className = 'btn btn-info btn-sm'; + logsBtn.innerHTML = ''; + logsBtn.title = 'Container-Logs anzeigen'; + logsBtn.onclick = () => showContainerLogs(container.id); + actionsDiv.appendChild(logsBtn); + + actionsCell.appendChild(actionsDiv); + row.appendChild(actionsCell); + + containerTable.appendChild(row); + }); + + // Container-Status-Diagramm aktualisieren + updateContainerStatusChart(data.containers); + }) + .catch(error => console.error('Fehler beim Abrufen der Docker-Informationen:', error)); +} + +// Initialisiere Container-Status-Diagramm +function initContainerStatusChart() { + const ctx = document.getElementById('container-status-chart').getContext('2d'); + + containerStatusChart = new Chart(ctx, { + type: 'pie', + data: { + labels: ['Aktiv', 'Inaktiv'], + datasets: [{ + data: [0, 0], + backgroundColor: [ + 'rgba(75, 192, 192, 0.7)', + 'rgba(255, 99, 132, 0.7)' + ] + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + title: { + display: true, + text: 'Container-Status', + font: { + size: 16 + } + } + } + } + }); +} + +// Aktualisiere Container-Status-Diagramm +function updateContainerStatusChart(containers) { + if (!containerStatusChart) return; + + const running = containers.filter(c => c.running).length; + const stopped = containers.filter(c => !c.running).length; + + containerStatusChart.data.datasets[0].data = [running, stopped]; + containerStatusChart.update(); +} + +// Container neustarten +function restartContainer(containerId) { + fetch(`/api/docker/restart/${containerId}`, { + method: 'POST' + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showMessage('Container wird neugestartet...', false); + // Nach kurzer Verzögerung aktualisieren + setTimeout(updateContainerStatus, 2000); + } else { + showMessage('Fehler beim Neustarten des Containers: ' + data.message, true); + } + }) + .catch(error => { + showMessage('Fehler beim Neustarten des Containers', true); + console.error('Fehler beim Neustarten des Containers:', error); + }); +} + +// Container-Logs anzeigen +function showContainerLogs(containerId) { + fetch(`/api/docker/logs/${containerId}`) + .then(response => response.json()) + .then(data => { + if (data.logs) { + // Modal erstellen und anzeigen + const modal = document.createElement('div'); + modal.className = 'modal'; + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); + + // Modal schließen, wenn auf X geklickt wird + modal.querySelector('.close').onclick = function() { + document.body.removeChild(modal); + }; + + // Modal schließen, wenn außerhalb geklickt wird + window.onclick = function(event) { + if (event.target === modal) { + document.body.removeChild(modal); + } + }; + + // Modal anzeigen + modal.style.display = 'block'; + } else { + showMessage('Keine Logs verfügbar', true); + } + }) + .catch(error => { + showMessage('Fehler beim Abrufen der Container-Logs', true); + console.error('Fehler beim Abrufen der Container-Logs:', error); + }); +} + +// Zeige Fehlermeldung oder Erfolgsmeldung +function showMessage(message, isError = false) { + const messageEl = document.getElementById('message'); + if (messageEl) { + messageEl.textContent = message; + messageEl.className = isError ? 'message message-error' : 'message message-success'; + messageEl.style.display = 'block'; + + // Verstecke Nachricht nach 5 Sekunden + setTimeout(() => { + messageEl.style.display = 'none'; + }, 5000); + } +} + +// Initialisiere alle Diagramme +function initAllCharts() { + // Überprüfe, ob Chart.js geladen ist + if (typeof Chart !== 'undefined') { + if (document.getElementById('cpu-usage-chart')) { + initCpuUsageChart(); + } + + if (document.getElementById('memory-usage-chart')) { + initMemoryUsageChart(); + } + + if (document.getElementById('disk-usage-chart')) { + initDiskUsageChart(); + } + + if (document.getElementById('container-status-chart')) { + initContainerStatusChart(); + } + + // Initialen Datenabruf starten + updateSystemCharts(); + updateContainerStatus(); + + // Regelmäßige Aktualisierung der Diagramme + setInterval(updateSystemCharts, 5000); // Alle 5 Sekunden aktualisieren + setInterval(updateContainerStatus, 10000); // Alle 10 Sekunden aktualisieren + } else { + console.error('Chart.js konnte nicht geladen werden.'); + } +} + +// Automatischer Start beim Laden der Seite +document.addEventListener('DOMContentLoaded', function() { + initAllCharts(); +}); \ No newline at end of file diff --git a/backend/debug-server/static/js/debug-dashboard.js b/backend/debug-server/static/js/debug-dashboard.js new file mode 100644 index 00000000..c8b914a5 --- /dev/null +++ b/backend/debug-server/static/js/debug-dashboard.js @@ -0,0 +1,1234 @@ +// Debug-Dashboard JavaScript + +// Globale Variablen für Charts +let cpuChart = null; +let memoryChart = null; +let diskChart = null; +let containerStatusChart = null; + +// Speicher für historische Daten +const cpuData = { + labels: Array(20).fill(''), + datasets: [{ + label: 'CPU-Auslastung (%)', + data: Array(20).fill(0), + borderColor: '#3498db', + backgroundColor: 'rgba(52, 152, 219, 0.2)', + tension: 0.4, + fill: true + }] +}; + +const memoryData = { + labels: ['Verwendet', 'Frei'], + datasets: [{ + data: [0, 100], + backgroundColor: ['#e74c3c', '#2ecc71'], + hoverBackgroundColor: ['#c0392b', '#27ae60'] + }] +}; + +// Gemeinsame Chart-Optionen +const lineChartOptions = { + responsive: true, + maintainAspectRatio: false, + animation: { + duration: 500 + }, + scales: { + y: { + beginAtZero: true, + max: 100, + ticks: { + callback: value => `${value}%` + } + } + }, + plugins: { + legend: { + display: true, + position: 'top' + } + } +}; + +const pieChartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'right' + } + } +}; + +// Helper-Funktionen +function showMessage(message, isError = false) { + const messageEl = document.getElementById('message'); + messageEl.textContent = message; + messageEl.className = isError ? 'message message-error' : 'message message-success'; + messageEl.style.display = 'block'; + + // Verstecke Nachricht nach 5 Sekunden + setTimeout(() => { + messageEl.style.display = 'none'; + }, 5000); +} + +function formatBytes(bytes, decimals = 2) { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} + +function formatUptime(seconds) { + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + + return `${days}d ${hours}h ${minutes}m`; +} + +// Dashboard-Initialisierung +document.addEventListener('DOMContentLoaded', function() { + // Tab-Wechsel einrichten + setupTabs(); + + // Charts initialisieren + initCharts(); + + // Daten laden + loadSystemMetrics(); + loadDockerStatus(); + refreshNetworkInterfaces(); + refreshActiveConnections(); + loadRouteTable(); + + // Regelmäßige Aktualisierungen + setInterval(loadSystemMetrics, 5000); + setInterval(loadDockerStatus, 10000); + + console.log('Debug-Dashboard initialisiert'); +}); + +// Tab-Funktionalität +function setupTabs() { + document.querySelectorAll('.tab').forEach(tab => { + tab.addEventListener('click', function() { + // Aktiven Tab-Status wechseln + document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); + this.classList.add('active'); + + // Tab-Inhalte wechseln + const tabId = this.getAttribute('data-tab'); + document.querySelectorAll('.tab-content').forEach(content => { + content.classList.remove('active'); + }); + document.getElementById(tabId + '-tab').classList.add('active'); + }); + }); +} + +// Chart-Initialisierung +function initCharts() { + // CPU-Chart + const cpuCtx = document.getElementById('cpu-usage-chart').getContext('2d'); + cpuChart = new Chart(cpuCtx, { + type: 'line', + data: cpuData, + options: lineChartOptions + }); + + // Memory-Chart + const memoryCtx = document.getElementById('memory-usage-chart').getContext('2d'); + memoryChart = new Chart(memoryCtx, { + type: 'doughnut', + data: memoryData, + options: pieChartOptions + }); + + // Disk-Chart (Wird später initialisiert, wenn Daten verfügbar sind) + + // Container-Status-Chart (Wird später initialisiert, wenn Daten verfügbar sind) +} + +// Daten-Lade-Funktionen +function loadSystemMetrics() { + fetch('/api/system/metrics') + .then(response => response.json()) + .then(data => { + if (data.success) { + updateSystemMetrics(data); + } + }) + .catch(error => { + console.error('Fehler beim Laden der Systemmetriken:', error); + }); +} + +function updateSystemMetrics(data) { + // CPU-Auslastung aktualisieren + document.getElementById('cpu-percent').textContent = `${data.cpu_percent.toFixed(1)}%`; + + // CPU-Chart aktualisieren + cpuData.datasets[0].data.shift(); + cpuData.datasets[0].data.push(data.cpu_percent); + cpuChart.update(); + + // RAM-Auslastung aktualisieren + document.getElementById('memory-percent').textContent = `${data.memory.percent.toFixed(1)}%`; + document.getElementById('memory-used').textContent = formatBytes(data.memory.used); + document.getElementById('memory-available').textContent = formatBytes(data.memory.available); + document.getElementById('memory-total').textContent = formatBytes(data.memory.total); + + // Memory-Chart aktualisieren + memoryData.datasets[0].data = [data.memory.percent, 100 - data.memory.percent]; + memoryChart.update(); + + // Disk-Chart aktualisieren oder initialisieren, wenn noch nicht vorhanden + if (data.disk_usage && data.disk_usage.length > 0) { + updateDiskChart(data.disk_usage); + } +} + +function updateDiskChart(diskData) { + if (!diskChart) { + // Chart initialisieren, wenn noch nicht vorhanden + const labels = diskData.map(disk => disk.mountpoint); + const usedData = diskData.map(disk => disk.used); + const freeData = diskData.map(disk => disk.free); + + const data = { + labels: labels, + datasets: [ + { + label: 'Verwendet', + data: usedData, + backgroundColor: 'rgba(231, 76, 60, 0.7)' + }, + { + label: 'Frei', + data: freeData, + backgroundColor: 'rgba(46, 204, 113, 0.7)' + } + ] + }; + + const options = { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + stacked: true + }, + y: { + stacked: true, + ticks: { + callback: value => formatBytes(value) + } + } + } + }; + + const diskCtx = document.getElementById('disk-usage-chart').getContext('2d'); + diskChart = new Chart(diskCtx, { + type: 'bar', + data: data, + options: options + }); + } else { + // Chart aktualisieren + diskChart.data.labels = diskData.map(disk => disk.mountpoint); + diskChart.data.datasets[0].data = diskData.map(disk => disk.used); + diskChart.data.datasets[1].data = diskData.map(disk => disk.free); + diskChart.update(); + } +} + +// Docker-Informationen laden +function loadDockerStatus() { + fetch('/api/docker/status') + .then(response => response.json()) + .then(data => { + if (data.success) { + updateDockerStatus(data); + } + }) + .catch(error => { + console.error('Fehler beim Laden des Docker-Status:', error); + }); +} + +function updateDockerStatus(data) { + // Docker-Version und Info aktualisieren + document.getElementById('docker-version').textContent = data.info.version || 'Nicht verfügbar'; + document.getElementById('docker-api-version').textContent = data.info.api_version || 'Nicht verfügbar'; + document.getElementById('docker-os').textContent = data.info.os || 'Nicht verfügbar'; + document.getElementById('docker-status').textContent = data.info.status || 'Nicht verfügbar'; + + // Anzahl aktiver Container aktualisieren + const activeContainers = data.containers.filter(c => c.state === 'running').length; + document.getElementById('active-containers').textContent = activeContainers; + + // Container-Status-Chart aktualisieren oder initialisieren + updateContainerStatusChart(data.containers); + + // Container-Tabelle aktualisieren + updateContainerTable(data.containers); + + // Container-Select für Logs aktualisieren + updateContainerSelect(data.containers); +} + +function updateContainerStatusChart(containers) { + // Zähle Container nach Status + const statusCounts = { + running: 0, + exited: 0, + created: 0, + other: 0 + }; + + containers.forEach(container => { + if (container.state === 'running') { + statusCounts.running++; + } else if (container.state === 'exited') { + statusCounts.exited++; + } else if (container.state === 'created') { + statusCounts.created++; + } else { + statusCounts.other++; + } + }); + + if (!containerStatusChart) { + // Chart initialisieren + const data = { + labels: ['Laufend', 'Beendet', 'Erstellt', 'Andere'], + datasets: [{ + data: [ + statusCounts.running, + statusCounts.exited, + statusCounts.created, + statusCounts.other + ], + backgroundColor: [ + '#2ecc71', // Grün für laufende Container + '#e74c3c', // Rot für beendete Container + '#3498db', // Blau für erstellte Container + '#95a5a6' // Grau für andere Status + ] + }] + }; + + const ctx = document.getElementById('container-status-chart').getContext('2d'); + containerStatusChart = new Chart(ctx, { + type: 'doughnut', + data: data, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'right' + } + } + } + }); + } else { + // Chart aktualisieren + containerStatusChart.data.datasets[0].data = [ + statusCounts.running, + statusCounts.exited, + statusCounts.created, + statusCounts.other + ]; + containerStatusChart.update(); + } +} + +function updateContainerTable(containers) { + const tableBody = document.querySelector('#container-table tbody'); + + if (containers.length === 0) { + tableBody.innerHTML = 'Keine Container gefunden'; + return; + } + + tableBody.innerHTML = ''; + + containers.forEach(container => { + const row = document.createElement('tr'); + row.className = `container-row ${container.state}`; + row.setAttribute('data-id', container.id); + + // Container-Status-Klasse + let statusClass = ''; + if (container.state === 'running') { + statusClass = 'status-good'; + } else if (container.state === 'exited') { + statusClass = 'status-error'; + } else { + statusClass = 'status-warning'; + } + + // Container-Name und Info + row.innerHTML = ` + +
${container.name}
+
${container.image}
+ + ${container.state} + ${container.cpu || 'N/A'} + ${container.memory ? formatBytes(container.memory) : 'N/A'} + ${container.ports || 'N/A'} + + + + + + `; + + tableBody.appendChild(row); + }); +} + +function updateContainerSelect(containers) { + const select = document.getElementById('log-container-select'); + + // Alle Einträge außer dem ersten leeren löschen + while (select.options.length > 1) { + select.remove(1); + } + + // Container sortieren (laufende zuerst) + const sortedContainers = [...containers].sort((a, b) => { + if (a.state === 'running' && b.state !== 'running') return -1; + if (a.state !== 'running' && b.state === 'running') return 1; + return a.name.localeCompare(b.name); + }); + + // Container zum Select hinzufügen + sortedContainers.forEach(container => { + const option = document.createElement('option'); + option.value = container.id; + option.textContent = `${container.name} (${container.state})`; + + // Laufende Container hervorheben + if (container.state === 'running') { + option.className = 'container-running'; + } + + select.appendChild(option); + }); +} + +// Container-Aktionen +function inspectContainer(containerId) { + fetch(`/api/docker/inspect/${containerId}`) + .then(response => response.json()) + .then(data => { + if (data.success) { + showContainerInspect(data.data); + } else { + showMessage(`Fehler: ${data.error}`, true); + } + }) + .catch(error => { + showMessage(`Fehler bei der Container-Inspektion: ${error}`, true); + }); +} + +function showContainerInspect(inspectData) { + // Hier können wir ein Modal oder einen anderen Bereich für die Anzeige der Inspektionsdaten implementieren + const detailsHTML = ` +
+

${inspectData.name}

+
${inspectData.id.substring(0, 12)}
+
+
+

Allgemein

+ + + + + +
Image${inspectData.image}
Erstellt${new Date(inspectData.created).toLocaleString()}
Status${inspectData.state.Status}
Gestartet${inspectData.state.StartedAt ? new Date(inspectData.state.StartedAt).toLocaleString() : 'Nicht gestartet'}
+
+
+

Netzwerk

+
+ ${Object.keys(inspectData.networks).map(net => ` +
+
${net}
+
IP: ${inspectData.networks[net].IPAddress}
+
Gateway: ${inspectData.networks[net].Gateway}
+
+ `).join('')} +
+
+
+

Ports

+
+ ${inspectData.ports ? Object.keys(inspectData.ports).map(port => ` +
+ ${port} → ${inspectData.ports[port] ? inspectData.ports[port].map(p => `${p.HostIp}:${p.HostPort}`).join(', ') : 'Nicht gemappt'} +
+ `).join('') : 'Keine Ports'} +
+
+ `; + + // Hier könnten wir ein Modal anzeigen oder die Daten in einen bestimmten Bereich einfügen + // Für dieses Beispiel verwenden wir ein einfaches Alert, aber in Produktion sollte ein ordentliches Modal verwendet werden + alert(`Container-Details:\n${inspectData.name}\n${inspectData.image}\nStatus: ${inspectData.state.Status}`); +} + +function restartContainer(containerId) { + if (confirm('Möchten Sie diesen Container wirklich neu starten?')) { + fetch(`/api/docker/restart/${containerId}`, { + method: 'POST' + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showMessage(`Container erfolgreich neu gestartet`, false); + setTimeout(() => loadDockerStatus(), 2000); + } else { + showMessage(`Fehler: ${data.error}`, true); + } + }) + .catch(error => { + showMessage(`Fehler beim Neustart des Containers: ${error}`, true); + }); + } +} + +function viewContainerLogs(containerId) { + // Container im Log-Select auswählen + document.getElementById('log-container-select').value = containerId; + + // Logs laden + loadContainerLogs(); +} + +function loadContainerLogs() { + const containerId = document.getElementById('log-container-select').value; + const filter = document.getElementById('log-filter').value; + + if (!containerId) { + showMessage('Bitte wählen Sie einen Container aus', true); + return; + } + + const logsContainer = document.getElementById('container-logs'); + logsContainer.innerHTML = '
Lade Logs...
'; + + fetch(`/api/docker/logs/${containerId}${filter ? `?filter=${filter}` : ''}`) + .then(response => response.json()) + .then(data => { + if (data.success) { + if (data.logs.length === 0) { + logsContainer.innerHTML = '
Keine Logs gefunden
'; + } else { + const logLines = data.logs.split('\n'); + + // Logs mit Syntax-Highlighting und Fehlerhervorhebung anzeigen + logsContainer.innerHTML = ` +
+
Logs für Container: ${data.container_name}
+
Zeilen: ${logLines.length}
+
+
${formatLogs(logLines)}
+ `; + } + } else { + logsContainer.innerHTML = `
Fehler: ${data.error}
`; + } + }) + .catch(error => { + logsContainer.innerHTML = `
Fehler beim Laden der Logs: ${error}
`; + }); +} + +function formatLogs(logLines) { + return logLines.map(line => { + // Zeitstempel hervorheben + line = line.replace(/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z)/g, '$1'); + + // Fehler hervorheben + if (/error|exception|fail|critical/i.test(line)) { + return `
${line}
`; + } + + // Warnungen hervorheben + if (/warning|warn/i.test(line)) { + return `
${line}
`; + } + + // Info-Meldungen hervorheben + if (/info|information/i.test(line)) { + return `
${line}
`; + } + + return `
${line}
`; + }).join(''); +} + +function downloadContainerLogs() { + const containerId = document.getElementById('log-container-select').value; + + if (!containerId) { + showMessage('Bitte wählen Sie einen Container aus', true); + return; + } + + // Direkter Download über einen Link + window.location.href = `/api/docker/logs/download/${containerId}`; +} + +// Netzwerk-Funktionen +function refreshNetworkInterfaces() { + const container = document.getElementById('network-interfaces'); + container.innerHTML = '
Lade Netzwerkschnittstellen...
'; + + fetch('/api/network/interfaces') + .then(response => response.json()) + .then(data => { + if (data.success) { + renderNetworkInterfaces(data.interfaces); + } else { + container.innerHTML = `
Fehler: ${data.error}
`; + } + }) + .catch(error => { + container.innerHTML = `
Fehler beim Laden der Netzwerkschnittstellen: ${error}
`; + }); +} + +function renderNetworkInterfaces(interfaces) { + const container = document.getElementById('network-interfaces'); + + if (interfaces.length === 0) { + container.innerHTML = '
Keine Netzwerkschnittstellen gefunden
'; + return; + } + + let html = ''; + + interfaces.forEach(iface => { + html += ` +
+
+ ${iface.name} + ${iface.mac} +
+ +
+

IPv4-Adressen

+ ${iface.ipv4.length > 0 ? iface.ipv4.map(ip => ` +
+
IP: ${ip.addr}
+
Netzmaske: ${ip.netmask}
+
Broadcast: ${ip.broadcast || 'N/A'}
+
+ `).join('') : '
Keine IPv4-Adressen
'} +
+ + ${iface.stats !== 'Nicht verfügbar' ? ` +
+

Statistiken

+
+
Gesendet: ${formatBytes(iface.stats.bytes_sent)}
+
Empfangen: ${formatBytes(iface.stats.bytes_recv)}
+
+
+
Pakete gesendet: ${iface.stats.packets_sent}
+
Pakete empfangen: ${iface.stats.packets_recv}
+
+
+
Fehler (Ein): ${iface.stats.errin}
+
Fehler (Aus): ${iface.stats.errout}
+
+
+ ` : ''} +
+ `; + }); + + container.innerHTML = html; +} + +function refreshActiveConnections() { + const tableBody = document.querySelector('#connections-table tbody'); + tableBody.innerHTML = 'Lade aktive Verbindungen...'; + + fetch('/api/network/active-connections') + .then(response => response.json()) + .then(data => { + if (data.success) { + renderActiveConnections(data.connections); + } else { + tableBody.innerHTML = `Fehler: ${data.error}`; + } + }) + .catch(error => { + tableBody.innerHTML = `Fehler beim Laden der aktiven Verbindungen: ${error}`; + }); +} + +function renderActiveConnections(connections) { + const tableBody = document.querySelector('#connections-table tbody'); + + if (connections.length === 0) { + tableBody.innerHTML = 'Keine aktiven Verbindungen gefunden'; + return; + } + + tableBody.innerHTML = ''; + + connections.forEach(conn => { + const row = document.createElement('tr'); + + row.innerHTML = ` + ${conn.local_address} + ${conn.remote_address} + ${conn.status} + ${conn.process ? `${conn.process.name} (PID: ${conn.pid})` : `PID: ${conn.pid || 'N/A'}`} + `; + + tableBody.appendChild(row); + }); +} + +function loadRouteTable() { + const container = document.getElementById('route-table'); + container.textContent = 'Lade Routing-Tabelle...'; + + fetch('/api/network/route-table') + .then(response => response.json()) + .then(data => { + if (data.success) { + container.textContent = data.route_table; + } else { + container.textContent = `Fehler: ${data.error}`; + } + }) + .catch(error => { + container.textContent = `Fehler beim Laden der Routing-Tabelle: ${error}`; + }); +} + +// Diagnose-Funktionen +function pingHost() { + const host = document.getElementById('ping-host').value; + if (!host) { + showMessage('Bitte geben Sie einen Host ein', true); + return; + } + + document.getElementById('ping-result').innerHTML = '
Ping wird durchgeführt...
'; + document.getElementById('ping-result').className = 'status'; + + fetch(`/ping/${host}`) + .then(response => response.json()) + .then(data => { + let html = `

Ping-Ergebnisse für ${host}

`; + html += `
${data.output}
`; + + document.getElementById('ping-result').innerHTML = html; + document.getElementById('ping-result').className = data.success ? 'status status-good' : 'status status-error'; + }) + .catch(error => { + document.getElementById('ping-result').innerHTML = `
Fehler beim Durchführen des Ping-Tests: ${error}
`; + document.getElementById('ping-result').className = 'status status-error'; + }); +} + +function tracerouteHost() { + const host = document.getElementById('traceroute-host').value; + if (!host) { + showMessage('Bitte geben Sie einen Host ein', true); + return; + } + + document.getElementById('traceroute-result').innerHTML = '
Traceroute wird durchgeführt...
'; + document.getElementById('traceroute-result').className = 'status'; + + fetch(`/traceroute/${host}`) + .then(response => response.json()) + .then(data => { + let html = `

Traceroute-Ergebnisse für ${host}

`; + html += `
${data.output}
`; + + if (data.error) { + html += `
Fehler: ${data.error}
`; + } + + document.getElementById('traceroute-result').innerHTML = html; + document.getElementById('traceroute-result').className = 'status'; + }) + .catch(error => { + document.getElementById('traceroute-result').innerHTML = `
Fehler beim Durchführen des Traceroute: ${error}
`; + document.getElementById('traceroute-result').className = 'status status-error'; + }); +} + +function dnsLookup() { + const host = document.getElementById('dns-host').value; + if (!host) { + showMessage('Bitte geben Sie einen Hostnamen ein', true); + return; + } + + document.getElementById('dns-result').innerHTML = '
DNS-Abfrage wird durchgeführt...
'; + document.getElementById('dns-result').className = 'status'; + + fetch(`/nslookup/${host}`) + .then(response => response.json()) + .then(data => { + let html = `

DNS-Abfrageergebnisse für ${host}

`; + html += `
${data.output}
`; + + if (data.error) { + html += `
Fehler: ${data.error}
`; + } + + document.getElementById('dns-result').innerHTML = html; + document.getElementById('dns-result').className = 'status'; + }) + .catch(error => { + document.getElementById('dns-result').innerHTML = `
Fehler bei der DNS-Abfrage: ${error}
`; + document.getElementById('dns-result').className = 'status status-error'; + }); +} + +function startPortScan() { + const host = document.getElementById('port-scan-host').value; + const portRange = document.getElementById('port-scan-range').value; + + if (!host) { + showMessage('Bitte geben Sie einen Host ein', true); + return; + } + + document.getElementById('port-scan-status').innerHTML = '
Port-Scan wird gestartet...
'; + document.getElementById('port-scan-status').className = 'status'; + + // Tabelle leeren + document.querySelector('#port-scan-table tbody').innerHTML = ''; + + fetch('/api/network/scan-ports', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + host: host, + port_range: portRange + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + document.getElementById('port-scan-status').innerHTML = data.message; + document.getElementById('port-scan-status').className = 'status status-good'; + + // Status-Polling starten + pollPortScanStatus(); + } else { + document.getElementById('port-scan-status').innerHTML = `
Fehler: ${data.error}
`; + document.getElementById('port-scan-status').className = 'status status-error'; + } + }) + .catch(error => { + document.getElementById('port-scan-status').innerHTML = `
Fehler beim Starten des Port-Scans: ${error}
`; + document.getElementById('port-scan-status').className = 'status status-error'; + }); +} + +function checkPortScanStatus() { + fetch('/api/network/scan-status') + .then(response => response.json()) + .then(data => { + if (data.success) { + if (data.status === 'running') { + document.getElementById('port-scan-status').innerHTML = '
Port-Scan läuft...
'; + document.getElementById('port-scan-status').className = 'status status-warning'; + } else if (data.status === 'completed' && data.results) { + renderPortScanResults(data.results); + } else { + document.getElementById('port-scan-status').innerHTML = data.message; + document.getElementById('port-scan-status').className = 'status'; + } + } else { + document.getElementById('port-scan-status').innerHTML = `
Fehler: ${data.error}
`; + document.getElementById('port-scan-status').className = 'status status-error'; + } + }) + .catch(error => { + document.getElementById('port-scan-status').innerHTML = `
Fehler beim Abrufen des Port-Scan-Status: ${error}
`; + document.getElementById('port-scan-status').className = 'status status-error'; + }); +} + +function pollPortScanStatus() { + const intervalId = setInterval(() => { + fetch('/api/network/scan-status') + .then(response => response.json()) + .then(data => { + if (data.success) { + if (data.status === 'running') { + document.getElementById('port-scan-status').innerHTML = '
Port-Scan läuft...
'; + document.getElementById('port-scan-status').className = 'status status-warning'; + } else { + // Scan ist abgeschlossen oder anderweitig beendet + clearInterval(intervalId); + + if (data.status === 'completed' && data.results) { + renderPortScanResults(data.results); + } else { + document.getElementById('port-scan-status').innerHTML = data.message; + document.getElementById('port-scan-status').className = 'status'; + } + } + } else { + clearInterval(intervalId); + document.getElementById('port-scan-status').innerHTML = `
Fehler: ${data.error}
`; + document.getElementById('port-scan-status').className = 'status status-error'; + } + }) + .catch(error => { + clearInterval(intervalId); + document.getElementById('port-scan-status').innerHTML = `
Fehler beim Abrufen des Port-Scan-Status: ${error}
`; + document.getElementById('port-scan-status').className = 'status status-error'; + }); + }, 1000); +} + +function renderPortScanResults(results) { + const tableBody = document.querySelector('#port-scan-table tbody'); + tableBody.innerHTML = ''; + + document.getElementById('port-scan-status').innerHTML = ` +
Scan für ${results.host} abgeschlossen
+
Ports: ${results.start_port}-${results.end_port}
+
Zeit: ${new Date(results.timestamp).toLocaleString()}
+
Offene Ports: ${results.open_ports.length}
+ `; + document.getElementById('port-scan-status').className = 'status status-good'; + + if (results.open_ports.length === 0) { + tableBody.innerHTML = 'Keine offenen Ports gefunden'; + return; + } + + results.open_ports.forEach(port => { + const row = document.createElement('tr'); + + row.innerHTML = ` + ${port.port} + ${port.status} + ${port.service} + `; + + tableBody.appendChild(row); + }); +} + +function loadLogs() { + const logType = document.getElementById('log-type').value; + const lines = document.getElementById('log-lines').value; + + const container = document.getElementById('log-content'); + container.innerHTML = '
Lade Logs...
'; + + fetch(`/api/logs/tail/${logType}?lines=${lines}`) + .then(response => response.json()) + .then(data => { + if (data.success) { + if (data.entries.length === 0) { + container.innerHTML = '
Keine Logs gefunden
'; + } else { + container.innerHTML = ` +
+
Logs für: ${logType}
+
Zeilen: ${data.count}
+
+
${formatLogEntries(data.entries)}
+ `; + } + } else { + container.innerHTML = `
Fehler: ${data.error}
`; + } + }) + .catch(error => { + container.innerHTML = `
Fehler beim Laden der Logs: ${error}
`; + }); +} + +function formatLogEntries(entries) { + return entries.map(line => { + // Zeitstempel hervorheben + line = line.replace(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(,\d+)?)/g, '$1'); + + // Fehler hervorheben + if (/error|exception|fail|critical/i.test(line)) { + return `
${line}
`; + } + + // Warnungen hervorheben + if (/warning|warn/i.test(line)) { + return `
${line}
`; + } + + // Info-Meldungen hervorheben + if (/info|information/i.test(line)) { + return `
${line}
`; + } + + // Debug-Meldungen hervorheben + if (/debug/i.test(line)) { + return `
${line}
`; + } + + return `
${line}
`; + }).join(''); +} + +function analyzeLogs() { + const logType = document.getElementById('log-type').value; + + const container = document.getElementById('log-content'); + container.innerHTML = '
Analysiere Logs...
'; + + fetch(`/api/logs/analyze?type=${logType}`) + .then(response => response.json()) + .then(data => { + if (data.success) { + if (data.analysis.error_count === 0 && data.analysis.warning_count === 0) { + container.innerHTML = '
Keine Fehler oder Warnungen in den Logs gefunden
'; + } else { + let html = ` +
+
Log-Analyse für: ${logType}
+
Fehler: ${data.analysis.error_count}, Warnungen: ${data.analysis.warning_count}
+
+ `; + + if (data.analysis.errors.length > 0) { + html += '

Fehler

'; + html += '
'; + data.analysis.errors.forEach(error => { + html += ` +
+
${error.timestamp}
+
${error.message}
+
+ `; + }); + html += '
'; + } + + if (data.analysis.warnings.length > 0) { + html += '

Warnungen

'; + html += '
'; + data.analysis.warnings.forEach(warning => { + html += ` +
+
${warning.timestamp}
+
${warning.message}
+
+ `; + }); + html += '
'; + } + + container.innerHTML = html; + } + } else { + container.innerHTML = `
Fehler: ${data.error}
`; + } + }) + .catch(error => { + container.innerHTML = `
Fehler bei der Log-Analyse: ${error}
`; + }); +} + +function checkHealth() { + const banner = document.getElementById('systemHealthBanner'); + banner.style.display = 'flex'; + banner.className = 'system-health-banner checking'; + banner.innerHTML = ` +
+
Systemstatus wird geprüft...
+ `; + + fetch('/healthcheck') + .then(response => response.json()) + .then(data => { + let statusClass = ''; + let icon = ''; + + if (data.status === 'healthy') { + statusClass = 'healthy'; + icon = ''; + } else if (data.status === 'warning') { + statusClass = 'warning'; + icon = ''; + } else { + statusClass = 'critical'; + icon = ''; + } + + let statusHTML = ` +
${icon}
+
+
Systemstatus: ${data.status.toUpperCase()}
+
+ `; + + // Einzelne Komponenten hinzufügen + Object.keys(data.checks).forEach(component => { + const check = data.checks[component]; + let componentIcon = ''; + + if (check.overall === 'healthy') { + componentIcon = ''; + } else if (check.overall === 'warning') { + componentIcon = ''; + } else { + componentIcon = ''; + } + + statusHTML += `
${componentIcon} ${component}: ${check.status}
`; + }); + + statusHTML += ` +
+
+ + `; + + banner.className = `system-health-banner ${statusClass}`; + banner.innerHTML = statusHTML; + + // Banner nach 10 Sekunden ausblenden, wenn es nicht kritisch ist + if (data.status !== 'critical') { + setTimeout(() => { + if (banner.className.includes(statusClass)) { + banner.style.display = 'none'; + } + }, 10000); + } + }) + .catch(error => { + banner.className = 'system-health-banner critical'; + banner.innerHTML = ` +
+
Fehler bei der Systemstatus-Prüfung: ${error}
+ + `; + }); +} + +function refreshPage() { + window.location.reload(); +} + +// Konfiguration +function saveConfig() { + const config = { + backend_hostname: document.getElementById('backend_hostname').value, + backend_port: parseInt(document.getElementById('backend_port').value, 10), + frontend_hostname: document.getElementById('frontend_hostname').value, + frontend_port: parseInt(document.getElementById('frontend_port').value, 10) + }; + + fetch('/save-config', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(config) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showMessage('Konfiguration erfolgreich gespeichert', false); + + // Nach kurzer Verzögerung die Seite neu laden + setTimeout(() => { + window.location.reload(); + }, 1500); + } else { + showMessage(`Fehler: ${data.message}`, true); + } + }) + .catch(error => { + showMessage(`Fehler: ${error}`, true); + }); +} + +function testConnection() { + const config = { + backend_hostname: document.getElementById('backend_hostname').value, + backend_port: parseInt(document.getElementById('backend_port').value, 10), + frontend_hostname: document.getElementById('frontend_hostname').value, + frontend_port: parseInt(document.getElementById('frontend_port').value, 10) + }; + + fetch('/test-connection', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(config) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + let message = 'Backend: '; + message += data.results.backend.ping ? 'Ping erfolgreich' : 'Ping fehlgeschlagen'; + message += ', '; + message += data.results.backend.connection ? 'Verbindung erfolgreich' : 'Verbindung fehlgeschlagen'; + message += ' | Frontend: '; + message += data.results.frontend.ping ? 'Ping erfolgreich' : 'Ping fehlgeschlagen'; + message += ', '; + message += data.results.frontend.connection ? 'Verbindung erfolgreich' : 'Verbindung fehlgeschlagen'; + + showMessage(message, false); + } else { + showMessage(`Fehler: ${data.message}`, true); + } + }) + .catch(error => { + showMessage(`Fehler: ${error}`, true); + }); +} + +function syncFrontend() { + fetch('/sync-frontend', { + method: 'POST' + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showMessage('Frontend erfolgreich synchronisiert', false); + } else { + showMessage(`Fehler: ${data.message}`, true); + } + }) + .catch(error => { + showMessage(`Fehler: ${error}`, true); + }); +} \ No newline at end of file diff --git a/backend/debug-server/templates/dashboard.html b/backend/debug-server/templates/dashboard.html new file mode 100644 index 00000000..d79f177c --- /dev/null +++ b/backend/debug-server/templates/dashboard.html @@ -0,0 +1,444 @@ + + + + + + MYP Debug Dashboard + + + + + + + + + +
+ + + + + +
+
+
+ Systemübersicht +
+
+
+
+
CPU-Auslastung
+
-
+
+ +
+
RAM-Auslastung
+
-
+
+ +
+
Aktive Container
+
-
+
+
+ +
+ +
+
+
+ +
+
+ Speichernutzung +
+
+
+ +
+ +
+
+
Verwendet
+
-
+
+ +
+
Verfügbar
+
-
+
+ +
+
Gesamt
+
-
+
+
+
+
+
+ +
+

Festplattennutzung

+ +
+ +
+
+ +
+

Netzwerkkonfiguration

+ +
+
+ Verbindungsstatus +
+
+

Backend

+
+ {{ backend_status }} +
+ +

Frontend

+
+ {{ frontend_status }} +
+
+
+ +
+
+ Netzwerkkonfiguration +
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+
+
+
+
+ +
+

Docker-Container

+ +
+
+
Container-Status
+
+
+ +
+
+
+ +
+
Docker-Info
+
+ + + + + + + + + + + + + + + + + +
Version-
API-Version-
OS-
Status-
+
+
+
+ +
+
Container-Liste
+
+
+ +
+ + + +
+
+ + + + + + + + + + + + + + + + + +
NameStatusCPUSpeicherNetzwerkAktionen
Lade Container-Informationen...
+
+
+ +
+
Docker-Logs Analyse
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
Container auswählen, um Logs anzuzeigen
+
+
+
+
+ +
+

Netzwerkschnittstellen

+ +
+
+ Netzwerkschnittstellen + +
+
+
Lade Netzwerkschnittstellen...
+
+
+ +
+
+ Aktive Verbindungen + +
+
+ + + + + + + + + + + + + + +
Lokale AdresseRemote-AdresseStatusProzess
Lade aktive Verbindungen...
+
+
+ +
+
+ Routing-Tabelle + +
+
+
Lade Routing-Tabelle...
+
+
+
+ +
+

Diagnose-Tools

+ +
+
Ping-Test
+
Traceroute
+
DNS-Abfrage
+
Port-Scan
+
Log-Analyse
+
+ +
+
+
+
+ +
+ +
+ +
+
+
+ +
+ Führen Sie einen Ping-Test durch, um Ergebnisse zu sehen. +
+
+
+
+ +
+
+
+
+ +
+ +
+ +
+
+
+ +
+ Führen Sie einen Traceroute-Test durch, um Ergebnisse zu sehen. +
+
+
+
+ +
+
+
+
+ +
+ +
+ +
+
+
+ +
+ Führen Sie eine DNS-Abfrage durch, um Ergebnisse zu sehen. +
+
+
+
+ +
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ Kein Port-Scan aktiv. +
+ +
+ + + + + + + + + + +
PortStatusDienst
+
+
+
+
+ +
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
Wählen Sie einen Log-Typ und klicken Sie auf "Logs laden"
+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/backend/debug-server/templates/debug.html b/backend/debug-server/templates/debug.html new file mode 100644 index 00000000..416c4474 --- /dev/null +++ b/backend/debug-server/templates/debug.html @@ -0,0 +1,261 @@ + + + + + + MYP Debug Server + + + +

MYP Debug Server

+

Letzte Aktualisierung: {{ last_check }}

+ +
+ +
+

Netzwerkkonfiguration

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+
+ +
+

Verbindungsstatus

+ +

Backend

+
+ {{ backend_status }} +
+ +

Frontend

+
+ {{ frontend_status }} +
+
+ +
+

Netzwerkschnittstellen

+ {% for interface in interfaces %} +
+ {{ interface.name }}: {{ interface.address }} +
+ {% endfor %} +
+ + + + \ No newline at end of file diff --git a/backend/frontend_v2_routes.py b/backend/frontend_v2_routes.py new file mode 100644 index 00000000..61ce26bd --- /dev/null +++ b/backend/frontend_v2_routes.py @@ -0,0 +1,346 @@ +""" +Routing-Modul für die neue Frontend V2-Implementierung. +Stellt die notwendigen Endpunkte bereit, während die Original-API-Endpunkte intakt bleiben. +""" + +from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify, g, session +import datetime +import os +from functools import wraps +import logging +import jwt + +# Blueprint für Frontend V2 erstellen +frontend_v2 = Blueprint('frontend_v2', __name__, template_folder='frontend_v2/templates') + +# Logger konfigurieren +logger = logging.getLogger(__name__) + +# Importiere Funktionen aus dem Hauptmodul +# Diese werden während der Registrierung des Blueprints übergeben, +# um zirkuläre Importe zu vermeiden +get_current_user = None +get_user_by_id = None +get_socket_by_id = None +get_job_by_id = None +get_all_sockets = None +get_all_users = None +get_all_jobs = None +get_jobs_by_user = None +login_required = None +admin_required = None +delete_session = None +socket_to_dict = None +job_to_dict = None +user_to_dict = None + +def set_app_functions(app_functions): + """ + Setzt die importierten Funktionen aus dem Hauptmodul. + + Args: + app_functions: Ein Dictionary mit Funktionen aus dem Hauptmodul + """ + global get_current_user, get_user_by_id, get_socket_by_id, get_job_by_id + global get_all_sockets, get_all_users, get_all_jobs, get_jobs_by_user + global login_required, admin_required, delete_session + global socket_to_dict, job_to_dict, user_to_dict + + get_current_user = app_functions.get('get_current_user') + get_user_by_id = app_functions.get('get_user_by_id') + get_socket_by_id = app_functions.get('get_socket_by_id') + get_job_by_id = app_functions.get('get_job_by_id') + get_all_sockets = app_functions.get('get_all_sockets') + get_all_users = app_functions.get('get_all_users') + get_all_jobs = app_functions.get('get_all_jobs') + get_jobs_by_user = app_functions.get('get_jobs_by_user') + login_required = app_functions.get('login_required') + admin_required = app_functions.get('admin_required') + delete_session = app_functions.get('delete_session') + socket_to_dict = app_functions.get('socket_to_dict') + job_to_dict = app_functions.get('job_to_dict') + user_to_dict = app_functions.get('user_to_dict') + +# Wrapper für Login-Erfordernis im Frontend +def frontend_login_required(f): + @wraps(f) + def decorated(*args, **kwargs): + user = get_current_user() + if not user: + flash('Bitte melden Sie sich an, um diese Seite zu besuchen.', 'error') + return redirect(url_for('frontend_v2.login')) + + g.current_user = user + return f(*args, **kwargs) + + return decorated + +# Wrapper für Admin-Erfordernis im Frontend +def frontend_admin_required(f): + @wraps(f) + def decorated(*args, **kwargs): + if not g.get('current_user') or g.current_user['role'] != 'admin': + flash('Sie benötigen Administrator-Rechte, um diese Seite zu besuchen.', 'error') + return redirect(url_for('frontend_v2.dashboard')) + return f(*args, **kwargs) + + return decorated + +# Öffentliche Routen +@frontend_v2.route('/') +def index(): + current_user = get_current_user() + if current_user: + return redirect(url_for('frontend_v2.dashboard')) + return redirect(url_for('frontend_v2.login')) + +@frontend_v2.route('/login') +def login(): + current_user = get_current_user() + if current_user: + return redirect(url_for('frontend_v2.dashboard')) + return render_template('login.html', current_user=None, active_page='login') + +@frontend_v2.route('/register') +def register(): + current_user = get_current_user() + if current_user: + return redirect(url_for('frontend_v2.dashboard')) + return render_template('register.html', current_user=None, active_page='register') + +@frontend_v2.route('/logout') +def logout(): + session_id = session.get('session_id') + if session_id: + delete_session(session_id) + session.pop('session_id', None) + + flash('Sie wurden erfolgreich abgemeldet.', 'success') + return redirect(url_for('frontend_v2.login')) + +# Geschützte Routen +@frontend_v2.route('/dashboard') +@frontend_login_required +def dashboard(): + current_user = g.current_user + current_year = datetime.datetime.now().year + return render_template('dashboard.html', + current_user=user_to_dict(current_user), + current_year=current_year, + active_page='dashboard') + +@frontend_v2.route('/jobs') +@frontend_login_required +def jobs(): + current_user = g.current_user + + # Admins sehen alle Jobs, normale Benutzer nur ihre eigenen + if current_user['role'] == 'admin': + all_jobs = get_all_jobs() + else: + all_jobs = get_jobs_by_user(current_user['id']) + + jobs_list = [job_to_dict(job) for job in all_jobs] + + # Sortiere Jobs nach Startzeit (neueste zuerst) + jobs_list.sort(key=lambda x: x['startAt'], reverse=True) + + # Gruppiere Jobs nach Status (aktiv, abgebrochen, abgeschlossen) + active_jobs = [job for job in jobs_list if job['remainingMinutes'] > 0 and not job['aborted']] + completed_jobs = [job for job in jobs_list if job['remainingMinutes'] == 0 and not job['aborted']] + aborted_jobs = [job for job in jobs_list if job['aborted']] + + return render_template('jobs.html', + current_user=user_to_dict(current_user), + active_jobs=active_jobs, + completed_jobs=completed_jobs, + aborted_jobs=aborted_jobs, + active_page='jobs') + +@frontend_v2.route('/job/') +@frontend_login_required +def job_details(job_id): + current_user = g.current_user + + job = get_job_by_id(job_id) + if not job: + flash('Der angeforderte Job wurde nicht gefunden.', 'error') + return redirect(url_for('frontend_v2.jobs')) + + # Benutzer können nur ihre eigenen Jobs sehen, es sei denn, sie sind Admins + if current_user['role'] != 'admin' and job['user_id'] != current_user['id']: + flash('Sie haben keine Berechtigung, diesen Job anzusehen.', 'error') + return redirect(url_for('frontend_v2.jobs')) + + job_data = job_to_dict(job) + + # Holen Sie sich die Drucker-Informationen + printer = get_socket_by_id(job['socket_id']) + printer_data = socket_to_dict(printer) if printer else None + + # Benutzerinformationen abrufen + job_user = get_user_by_id(job['user_id']) + job_user_data = user_to_dict(job_user) if job_user else None + + return render_template('job_details.html', + current_user=user_to_dict(current_user), + job=job_data, + printer=printer_data, + job_user=job_user_data, + active_page='jobs') + +@frontend_v2.route('/profile') +@frontend_login_required +def profile(): + current_user = g.current_user + + # Benutzer-Jobs abrufen + user_jobs = get_jobs_by_user(current_user['id']) + jobs_list = [job_to_dict(job) for job in user_jobs] + + # Jobs nach Startzeit sortieren (neueste zuerst) + jobs_list.sort(key=lambda x: x['startAt'], reverse=True) + + # Gruppiere Jobs nach Status (aktiv, abgebrochen, abgeschlossen) + active_jobs = [job for job in jobs_list if job['remainingMinutes'] > 0 and not job['aborted']] + recent_jobs = jobs_list[:5] # Die 5 neuesten Jobs + + # Nutzungsstatistiken berechnen + total_jobs = len(jobs_list) + total_minutes_used = sum(job['durationInMinutes'] for job in jobs_list if not job['aborted']) + avg_duration = total_minutes_used // total_jobs if total_jobs > 0 else 0 + + return render_template('profile.html', + current_user=user_to_dict(current_user), + active_jobs=active_jobs, + recent_jobs=recent_jobs, + total_jobs=total_jobs, + total_minutes_used=total_minutes_used, + avg_duration=avg_duration, + active_page='profile') + +# Admin-Routen +@frontend_v2.route('/printers') +@frontend_login_required +@frontend_admin_required +def printers(): + current_user = g.current_user + + # Alle Drucker abrufen + all_sockets = get_all_sockets() + printers_list = [socket_to_dict(socket) for socket in all_sockets] + + return render_template('printers.html', + current_user=user_to_dict(current_user), + printers=printers_list, + active_page='printers') + +@frontend_v2.route('/printer/') +@frontend_login_required +@frontend_admin_required +def printer_details(printer_id): + current_user = g.current_user + + printer = get_socket_by_id(printer_id) + if not printer: + flash('Der angeforderte Drucker wurde nicht gefunden.', 'error') + return redirect(url_for('frontend_v2.printers')) + + printer_data = socket_to_dict(printer) + + return render_template('printer_details.html', + current_user=user_to_dict(current_user), + printer=printer_data, + active_page='printers') + +@frontend_v2.route('/users') +@frontend_login_required +@frontend_admin_required +def users(): + current_user = g.current_user + + # Alle Benutzer abrufen + all_users = get_all_users() + users_list = [user_to_dict(user) for user in all_users] + + return render_template('users.html', + current_user=user_to_dict(current_user), + users=users_list, + active_page='users') + +@frontend_v2.route('/user/') +@frontend_login_required +@frontend_admin_required +def user_details(user_id): + current_user = g.current_user + + user = get_user_by_id(user_id) + if not user: + flash('Der angeforderte Benutzer wurde nicht gefunden.', 'error') + return redirect(url_for('frontend_v2.users')) + + user_data = user_to_dict(user) + + # Benutzer-Jobs abrufen + user_jobs = get_jobs_by_user(user_id) + jobs_list = [job_to_dict(job) for job in user_jobs] + + # Jobs nach Startzeit sortieren (neueste zuerst) + jobs_list.sort(key=lambda x: x['startAt'], reverse=True) + + # Gruppiere Jobs nach Status + active_jobs = [job for job in jobs_list if job['remainingMinutes'] > 0 and not job['aborted']] + completed_jobs = [job for job in jobs_list if job['remainingMinutes'] == 0 and not job['aborted']] + aborted_jobs = [job for job in jobs_list if job['aborted']] + + # Nutzungsstatistiken berechnen + total_jobs = len(jobs_list) + total_minutes_used = sum(job['durationInMinutes'] for job in jobs_list if not job['aborted']) + avg_duration = total_minutes_used // total_jobs if total_jobs > 0 else 0 + + return render_template('user_details.html', + current_user=user_to_dict(current_user), + user=user_data, + active_jobs=active_jobs, + completed_jobs=completed_jobs, + aborted_jobs=aborted_jobs, + total_jobs=total_jobs, + total_minutes_used=total_minutes_used, + avg_duration=avg_duration, + active_page='users') + +@frontend_v2.route('/statistics') +@frontend_login_required +@frontend_admin_required +def statistics(): + current_user = g.current_user + + return render_template('statistics.html', + current_user=user_to_dict(current_user), + active_page='statistics') + +# Fehlerbehandlung +@frontend_v2.errorhandler(404) +def page_not_found(e): + current_user = get_current_user() + return render_template('error.html', + current_user=user_to_dict(current_user) if current_user else None, + error_code=404, + error_message='Die angeforderte Seite wurde nicht gefunden.'), 404 + +@frontend_v2.errorhandler(403) +def forbidden(e): + current_user = get_current_user() + return render_template('error.html', + current_user=user_to_dict(current_user) if current_user else None, + error_code=403, + error_message='Sie haben keine Berechtigung, auf diese Seite zuzugreifen.'), 403 + +@frontend_v2.errorhandler(500) +def server_error(e): + current_user = get_current_user() + logger.error(f'Serverfehler: {e}') + return render_template('error.html', + current_user=user_to_dict(current_user) if current_user else None, + error_code=500, + error_message='Ein interner Serverfehler ist aufgetreten.'), 500 \ No newline at end of file diff --git a/backend/install.sh b/backend/install.sh new file mode 100644 index 00000000..d67d86c5 --- /dev/null +++ b/backend/install.sh @@ -0,0 +1,510 @@ +#!/bin/bash + +# MYP Backend Installations-Skript +# Dieses Skript installiert das Backend mit Docker und Host-Netzwerkanbindung + +# Farbcodes für Ausgabe +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Funktion zur Ausgabe mit Zeitstempel +log() { + echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" +} + +error_log() { + echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] FEHLER:${NC} $1" >&2 +} + +# Funktion zum Bereinigen vorhandener Installationen +cleanup_existing_installation() { + log "${YELLOW}Bereinige vorhandene Installation...${NC}" + + # Stoppe und entferne existierende Container + if docker ps -a | grep -q "myp-backend"; then + log "Stoppe und entferne existierenden Backend-Container..." + docker stop myp-backend &>/dev/null || true + docker rm myp-backend &>/dev/null || true + fi + + # Entferne Docker Images + if docker images | grep -q "myp-backend"; then + log "Entferne existierendes Backend-Image..." + docker rmi myp-backend &>/dev/null || true + fi + + log "${GREEN}Bereinigung abgeschlossen.${NC}" +} + +# Pfade definieren +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +BACKEND_DIR="$SCRIPT_DIR" + +# Bereinige existierende Installation +cleanup_existing_installation + +# Funktion zur Installation von Docker und Docker Compose für Raspberry Pi +install_docker() { + log "${YELLOW}Docker ist nicht installiert. Installation wird gestartet...${NC}" + + # Erkenne Raspberry Pi + if [ -f /proc/device-tree/model ] && grep -q "Raspberry Pi" /proc/device-tree/model; then + log "${GREEN}Raspberry Pi erkannt. Installiere Docker für ARM-Architektur...${NC}" + IS_RASPBERRY_PI=true + else + IS_RASPBERRY_PI=false + fi + + # Aktualisiere Paketindex + if ! sudo apt-get update; then + error_log "Konnte Paketindex nicht aktualisieren. Bitte manuell installieren." + exit 1 + fi + + # Installiere erforderliche Pakete + if ! sudo apt-get install -y apt-transport-https ca-certificates curl gnupg software-properties-common; then + error_log "Konnte erforderliche Pakete nicht installieren. Bitte manuell installieren." + exit 1 + fi + + # Raspberry Pi-spezifische Installation + if [ "$IS_RASPBERRY_PI" = true ]; then + # Setze Systemarchitektur für Raspberry Pi (armhf oder arm64) + ARCH=$(dpkg --print-architecture) + log "Erkannte Systemarchitektur: ${ARCH}" + + # Installiere Docker mit convenience script (für Raspberry Pi empfohlen) + log "${YELLOW}Installiere Docker mit dem convenience script...${NC}" + curl -fsSL https://get.docker.com -o get-docker.sh + sudo sh get-docker.sh + + if [ $? -ne 0 ]; then + error_log "Docker-Installation fehlgeschlagen. Bitte manuell installieren." + exit 1 + fi + else + # Standard-Installation für andere Systeme + # Füge Docker's offiziellen GPG-Schlüssel hinzu + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - + + # Füge Docker-Repository hinzu + if ! sudo add-apt-repository "deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"; then + error_log "Konnte Docker-Repository nicht hinzufügen. Prüfen Sie, ob Ihr System unterstützt wird." + exit 1 + fi + + # Aktualisiere Paketindex erneut + sudo apt-get update + + # Installiere Docker + if ! sudo apt-get install -y docker-ce docker-ce-cli containerd.io; then + error_log "Konnte Docker nicht installieren. Bitte manuell installieren." + exit 1 + fi + fi + + # Füge aktuellen Benutzer zur Docker-Gruppe hinzu + sudo usermod -aG docker "$USER" + + log "${GREEN}Docker wurde installiert.${NC}" + log "${YELLOW}WICHTIG: Möglicherweise müssen Sie sich neu anmelden, damit die Gruppenänderung wirksam wird.${NC}" + + # Prüfen, ob Docker Compose v2 Plugin verfügbar ist (bevorzugt, da moderner) + log "${YELLOW}Prüfe Docker Compose Version...${NC}" + + if docker compose version &> /dev/null; then + log "${GREEN}Docker Compose v2 Plugin ist bereits installiert.${NC}" + DOCKER_COMPOSE_V2=true + else + log "${YELLOW}Docker Compose v2 Plugin nicht gefunden. Versuche Docker Compose v1 zu installieren...${NC}" + DOCKER_COMPOSE_V2=false + + if [ "$IS_RASPBERRY_PI" = true ]; then + # Für Raspberry Pi ist es besser, die richtige Architektur zu verwenden + if [ "$ARCH" = "armhf" ]; then + log "Installiere Docker Compose für armhf (32-bit)..." + sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-armv7" -o /usr/local/bin/docker-compose + elif [ "$ARCH" = "arm64" ]; then + log "Installiere Docker Compose für arm64 (64-bit)..." + sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-aarch64" -o /usr/local/bin/docker-compose + else + # Fallback auf v1.29.2 für unbekannte ARM-Architekturen + log "Verwende automatische Architekturerkennung für Docker Compose v1.29.2..." + sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + fi + else + # Für andere Systeme versuche zuerst v2, dann v1.29.2 als Fallback + log "Installiere Docker Compose v2 für $(uname -s)/$(uname -m)..." + if ! sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose; then + log "${YELLOW}Konnte Docker Compose v2 nicht herunterladen. Versuche v1.29.2...${NC}" + sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + fi + fi + + if [ $? -ne 0 ]; then + error_log "Konnte Docker Compose nicht herunterladen. Bitte manuell installieren." + exit 1 + fi + + sudo chmod +x /usr/local/bin/docker-compose + + log "${GREEN}Docker Compose wurde installiert.${NC}" + fi + + # Starte Docker-Dienst + if command -v systemctl &> /dev/null; then + sudo systemctl enable docker + sudo systemctl start docker + elif command -v service &> /dev/null; then + sudo service docker enable + sudo service docker start + fi +} + +# Prüfen ob Docker installiert ist +if ! command -v docker &> /dev/null; then + log "${YELLOW}Docker ist nicht installiert.${NC}" + read -p "Möchten Sie Docker installieren? (j/n): " install_docker_choice + if [[ "$install_docker_choice" == "j" ]]; then + install_docker + else + error_log "Docker wird für die Installation benötigt. Bitte installieren Sie Docker manuell." + log "Siehe: https://docs.docker.com/get-docker/" + exit 1 + fi +fi + +# Prüfen ob Docker Daemon läuft +if ! docker info &> /dev/null; then + log "${YELLOW}Docker-Daemon läuft nicht. Versuche, den Dienst zu starten...${NC}" + + # Versuche, Docker zu starten + if command -v systemctl &> /dev/null; then + sudo systemctl start docker + elif command -v service &> /dev/null; then + sudo service docker start + else + error_log "Konnte Docker-Daemon nicht starten. Bitte starten Sie den Docker-Dienst manuell." + log "Starten mit: sudo systemctl start docker oder sudo service docker start" + exit 1 + fi + + # Prüfe erneut, ob Docker läuft + if ! docker info &> /dev/null; then + error_log "Docker-Daemon konnte nicht gestartet werden. Bitte starten Sie den Docker-Dienst manuell." + exit 1 + fi + + log "${GREEN}Docker-Daemon wurde erfolgreich gestartet.${NC}" +fi + +# Prüfen ob Docker Compose installiert ist +if docker compose version &> /dev/null; then + log "${GREEN}Docker Compose v2 Plugin ist bereits installiert.${NC}" + DOCKER_COMPOSE_V2=true +elif command -v docker-compose &> /dev/null; then + log "${GREEN}Docker Compose v1 ist bereits installiert.${NC}" + DOCKER_COMPOSE_V2=false +else + log "${YELLOW}Docker Compose ist nicht installiert.${NC}" + DOCKER_COMPOSE_V2=false + read -p "Möchten Sie Docker Compose installieren? (j/n): " install_compose_choice + if [[ "$install_compose_choice" == "j" ]]; then + log "${YELLOW}Installiere Docker Compose...${NC}" + + # Prüfe ob das Betriebssystem ARM-basiert ist (z.B. Raspberry Pi) + if grep -q "arm" /proc/cpuinfo 2> /dev/null; then + ARCH=$(dpkg --print-architecture 2> /dev/null || echo "unknown") + IS_RASPBERRY_PI=true + else + IS_RASPBERRY_PI=false + fi + + # Versuche zuerst Docker Compose v2 zu installieren + if [ "$IS_RASPBERRY_PI" = true ]; then + if [ "$ARCH" = "armhf" ]; then + log "Installiere Docker Compose für armhf (32-bit)..." + sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-armv7" -o /usr/local/bin/docker-compose + elif [ "$ARCH" = "arm64" ]; then + log "Installiere Docker Compose für arm64 (64-bit)..." + sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-aarch64" -o /usr/local/bin/docker-compose + else + log "Verwende automatische Architekturerkennung für Docker Compose v1.29.2..." + sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + fi + else + log "Installiere Docker Compose v2 für $(uname -s)/$(uname -m)..." + if ! sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose; then + log "${YELLOW}Konnte Docker Compose v2 nicht herunterladen. Versuche v1.29.2...${NC}" + sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + fi + fi + + if [ $? -ne 0 ]; then + error_log "Konnte Docker Compose nicht herunterladen. Bitte manuell installieren." + exit 1 + fi + + sudo chmod +x /usr/local/bin/docker-compose + + log "${GREEN}Docker Compose wurde installiert.${NC}" + else + error_log "Docker Compose wird für die Installation benötigt. Bitte installieren Sie es manuell." + log "Siehe: https://docs.docker.com/compose/install/" + exit 1 + fi +fi + +# Prüfen ob wget installiert ist (wird für healthcheck verwendet) +if ! command -v wget &> /dev/null; then + error_log "wget ist nicht installiert, wird aber für den Container-Healthcheck benötigt." + log "Installation mit: sudo apt-get install wget" + exit 1 +fi + +# Wechsle ins Backend-Verzeichnis +log "Wechsle ins Verzeichnis: $BACKEND_DIR" +cd "$BACKEND_DIR" || { + error_log "Konnte nicht ins Verzeichnis $BACKEND_DIR wechseln." + exit 1 +} + +# Prüfe ob Dockerfile existiert +if [ ! -f "Dockerfile" ]; then + error_log "Dockerfile nicht gefunden in $BACKEND_DIR." + exit 1 +fi + +# Prüfe ob docker-compose.yml existiert +if [ ! -f "docker-compose.yml" ]; then + error_log "docker-compose.yml nicht gefunden in $BACKEND_DIR." + exit 1 +fi + +# Erstelle .env-Datei +log "${YELLOW}Erstelle .env Datei...${NC}" +cat > .env << EOL +SECRET_KEY=7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F +DATABASE_PATH=instance/myp.db +TAPO_USERNAME=till.tomczak@mercedes-benz.com +TAPO_PASSWORD=744563017196A +PRINTERS={"Printer 1": {"ip": "192.168.0.100"}, "Printer 2": {"ip": "192.168.0.101"}, "Printer 3": {"ip": "192.168.0.102"}, "Printer 4": {"ip": "192.168.0.103"}, "Printer 5": {"ip": "192.168.0.104"}, "Printer 6": {"ip": "192.168.0.106"}} +EOL + +if [ ! -f ".env" ]; then + error_log "Konnte .env-Datei nicht erstellen. Prüfen Sie die Berechtigungen." + exit 1 +fi +log "${GREEN}.env Datei erfolgreich erstellt${NC}" + +# Verzeichnisse erstellen +log "Erstelle benötigte Verzeichnisse" +if ! mkdir -p logs; then + error_log "Konnte Verzeichnis 'logs' nicht erstellen. Prüfen Sie die Berechtigungen." + exit 1 +fi + +if ! mkdir -p instance; then + error_log "Konnte Verzeichnis 'instance' nicht erstellen. Prüfen Sie die Berechtigungen." + exit 1 +fi + +# Docker-Image bauen und starten +log "${YELLOW}Baue und starte Backend-Container...${NC}" +log "${YELLOW}Dies kann auf einem Raspberry Pi einige Minuten dauern - bitte geduldig sein${NC}" + +# Prüfe, ob Docker-Daemon läuft +if ! docker info &>/dev/null; then + log "${YELLOW}Docker-Daemon scheint nicht zu laufen. Versuche zu starten...${NC}" + + # Versuche Docker zu starten + if command -v systemctl &>/dev/null; then + sudo systemctl start docker || true + sleep 5 + elif command -v service &>/dev/null; then + sudo service docker start || true + sleep 5 + fi + + # Prüfe erneut, ob Docker jetzt läuft + if ! docker info &>/dev/null; then + error_log "Docker-Daemon konnte nicht gestartet werden." + log "Führen Sie vor der Installation bitte folgende Befehle aus:" + log " sudo systemctl start docker" + log " sudo systemctl enable docker" + log "Starten Sie dann das Installationsskript erneut." + exit 1 + fi +fi + +# Docker-Rechte prüfen +if ! docker ps &>/dev/null; then + error_log "Sie haben keine Berechtigung, Docker ohne sudo zu verwenden." + log "Bitte führen Sie folgenden Befehl aus und melden Sie sich danach neu an:" + log " sudo usermod -aG docker $USER" + exit 1 +fi + +# Prüfen, ob erforderliche Basis-Images lokal verfügbar sind +if ! docker image inspect python:3-slim &>/dev/null; then + log "${YELLOW}Prüfe und setze DNS-Server für Docker...${NC}" + + # DNS-Einstellungen prüfen und anpassen + if [ -f /etc/docker/daemon.json ]; then + log "Bestehende Docker-Konfiguration gefunden." + else + log "Erstelle Docker-Konfiguration mit Google DNS..." + sudo mkdir -p /etc/docker + echo '{ + "dns": ["8.8.8.8", "8.8.4.4"] +}' | sudo tee /etc/docker/daemon.json > /dev/null + + # Docker neu starten, damit die Änderungen wirksam werden + if command -v systemctl &>/dev/null; then + sudo systemctl restart docker + sleep 5 + elif command -v service &>/dev/null; then + sudo service docker restart + sleep 5 + fi + fi + + # Versuche Image explizit mit anderen Tags herunterzuladen + log "${YELLOW}Versuche lokal vorhandene Python-Version zu finden...${NC}" + + # Suche nach allen verfügbaren Python-Images + PYTHON_IMAGES=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep "python:") + + if [ -n "$PYTHON_IMAGES" ]; then + log "Gefundene Python-Images: $PYTHON_IMAGES" + # Verwende das erste gefundene Python-Image + FIRST_PYTHON=$(echo "$PYTHON_IMAGES" | head -n 1) + log "${GREEN}Verwende vorhandenes Python-Image: $FIRST_PYTHON${NC}" + + # Aktualisiere den Dockerfile + sed -i "s|FROM python:3-slim|FROM $FIRST_PYTHON|g" Dockerfile + log "Dockerfile aktualisiert, um lokales Image zu verwenden." + else + # Versuche unterschiedliche Python-Versionen + for PYTHON_VERSION in "python:3.11-slim" "python:3.10-slim" "python:3.9-slim" "python:slim" "python:alpine"; do + log "Versuche $PYTHON_VERSION zu laden..." + if docker pull $PYTHON_VERSION; then + log "${GREEN}Erfolgreich $PYTHON_VERSION heruntergeladen${NC}" + # Aktualisiere den Dockerfile + sed -i "s|FROM python:3-slim|FROM $PYTHON_VERSION|g" Dockerfile + log "Dockerfile aktualisiert, um $PYTHON_VERSION zu verwenden." + break + fi + done + fi +fi + +# Erhöhe Docker-Timeout für langsame Verbindungen und Raspberry Pi +export DOCKER_CLIENT_TIMEOUT=300 +export COMPOSE_HTTP_TIMEOUT=300 + +# Verwende die richtige Docker Compose Version +if [ "${DOCKER_COMPOSE_V2:-false}" = true ]; then + # Docker Compose V2 Plugin (docker compose) + log "Baue lokales Image..." + if ! docker compose build --no-cache; then + error_log "Docker Compose Build (v2) fehlgeschlagen. Versuche mit v1 Format..." + if ! docker-compose build --no-cache; then + error_log "Docker Compose Build fehlgeschlagen. Siehe Fehlermeldung oben." + exit 1 + fi + fi + + log "Starte Container aus lokalem Image..." + if ! docker compose up -d; then + error_log "Docker Compose Up (v2) fehlgeschlagen. Versuche mit v1 Format..." + if ! docker-compose up -d; then + error_log "Docker Compose Up fehlgeschlagen. Siehe Fehlermeldung oben." + exit 1 + fi + fi +else + # Docker Compose V1 (docker-compose) + log "Baue lokales Image..." + if ! docker-compose build --no-cache; then + error_log "Docker Compose Build fehlgeschlagen. Siehe Fehlermeldung oben." + exit 1 + fi + + log "Starte Container aus lokalem Image..." + if ! docker-compose up -d; then + error_log "Docker Compose Up fehlgeschlagen. Siehe Fehlermeldung oben." + exit 1 + fi +fi + +# Prüfe, ob der Container läuft +log "Warte 10 Sekunden, bis der Container gestartet ist..." +sleep 10 +if docker ps | grep -q "myp-backend"; then + log "${GREEN}Backend-Container läuft${NC}" +else + error_log "Backend-Container läuft nicht. Container-Status:" + docker ps -a | grep myp-backend + log "Container-Logs:" + docker logs myp-backend + exit 1 +fi + +# Test API-Endpunkt +log "${YELLOW}Teste Backend-API...${NC}" +log "${YELLOW}HINWEIS: Der API-Server ist bei der ersten Installation oft noch nicht erreichbar${NC}" +log "${YELLOW}Dies ist ein bekanntes Verhalten wegen der Netzwerkkonfiguration${NC}" +log "${YELLOW}Bitte nach der Installation das System neu starten, danach sollte der API-Server erreichbar sein${NC}" + +# Wir versuchen es trotzdem einmal, um zu sehen, ob er vielleicht doch läuft +if curl -s http://localhost:5000/health 2>/dev/null | grep -q "healthy"; then + log "${GREEN}Backend-API ist erreichbar und funktioniert${NC}" +else + log "${YELLOW}Backend-API ist wie erwartet noch nicht erreichbar${NC}" + log "${GREEN}Das ist völlig normal bei der Erstinstallation${NC}" + log "${GREEN}Nach einem Neustart des Systems sollte der API-Server korrekt erreichbar sein${NC}" + log "Container-Status prüfen mit: docker logs myp-backend" +fi + +# Initialisierung der Datenbank prüfen +log "${YELLOW}Prüfe Datenbank-Initialisierung...${NC}" +if [ ! -s "instance/myp.db" ]; then + log "${YELLOW}Datenbank scheint leer zu sein. Führe Initialisierungsskript aus...${NC}" + DB_INIT_OUTPUT=$(docker exec myp-backend python -c "from app import init_db; init_db()" 2>&1) + if [ $? -eq 0 ]; then + log "${GREEN}Datenbank erfolgreich initialisiert${NC}" + else + error_log "Fehler bei der Datenbank-Initialisierung:" + echo "$DB_INIT_OUTPUT" + log "Container-Logs:" + docker logs myp-backend + fi +else + log "${GREEN}Datenbank existiert bereits${NC}" +fi + +# Teste, ob ein API-Endpunkt Daten zurückgibt +log "${YELLOW}Teste Datenbank-Verbindung über API...${NC}" +if curl -s http://localhost:5000/api/printers | grep -q "\[\]"; then + log "${GREEN}Datenbank-Verbindung funktioniert${NC}" +else + log "${YELLOW}API gibt keine leere Drucker-Liste zurück. Möglicherweise ist die DB nicht korrekt initialisiert.${NC}" + log "API-Antwort:" + curl -s http://localhost:5000/api/printers +fi + +log "${GREEN}=== Installation abgeschlossen ===${NC}" +log "${YELLOW}WICHTIG: Nach der Erstinstallation ist ein Systemneustart erforderlich${NC}" +log "${YELLOW}Danach ist das Backend unter http://localhost:5000 erreichbar${NC}" +log "Anzeigen der Logs: docker logs -f myp-backend" + +# Verwende die richtige Docker Compose Version für Hinweis +if [ "${DOCKER_COMPOSE_V2:-false}" = true ]; then + log "Backend stoppen: docker compose -f $BACKEND_DIR/docker-compose.yml down" +else + log "Backend stoppen: docker-compose -f $BACKEND_DIR/docker-compose.yml down" +fi \ No newline at end of file diff --git a/backend/network_config.py b/backend/network_config.py new file mode 100644 index 00000000..8432e94b --- /dev/null +++ b/backend/network_config.py @@ -0,0 +1,185 @@ +import os +import json +import socket +import subprocess +import platform +import netifaces +import requests +from datetime import datetime +import logging + +class NetworkConfig: + """Verwaltet die Netzwerkkonfiguration für das MYP-System.""" + + CONFIG_FILE = 'instance/network_config.json' + DEFAULT_CONFIG = { + 'backend_hostname': '192.168.0.5', + 'backend_port': 5000, + 'frontend_hostname': '192.168.0.106', + 'frontend_port': 3000 + } + + def __init__(self, app=None): + """Initialisierung der Netzwerkkonfiguration.""" + self.logger = logging.getLogger('myp.network') + self.config = self.DEFAULT_CONFIG.copy() + self.last_check = None + self.backend_status = "Nicht überprüft" + self.frontend_status = "Nicht überprüft" + + # Stelle sicher, dass das Verzeichnis existiert + os.makedirs(os.path.dirname(self.CONFIG_FILE), exist_ok=True) + + # Lade gespeicherte Konfiguration, falls vorhanden + self.load_config() + + if app: + self.init_app(app) + + def init_app(self, app): + """Initialisiert die Anwendung mit dieser Konfiguration.""" + app.network_config = self + + # Registriere Route für Netzwerkkonfiguration + @app.route('/admin/network-config', methods=['GET', 'POST']) + def network_config(): + from flask import request, render_template, flash, redirect, url_for + + # Prüfe aktuelle Status + self.check_connection_statuses() + + if request.method == 'POST': + # Aktualisiere Konfiguration + self.config['backend_hostname'] = request.form.get('backend_hostname', self.DEFAULT_CONFIG['backend_hostname']) + self.config['backend_port'] = int(request.form.get('backend_port', self.DEFAULT_CONFIG['backend_port'])) + self.config['frontend_hostname'] = request.form.get('frontend_hostname', self.DEFAULT_CONFIG['frontend_hostname']) + self.config['frontend_port'] = int(request.form.get('frontend_port', self.DEFAULT_CONFIG['frontend_port'])) + + # Speichere Konfiguration + self.save_config() + + # Teste die neue Konfiguration + self.check_connection_statuses() + + flash('Netzwerkkonfiguration erfolgreich gespeichert!', 'success') + return redirect(url_for('network_config')) + + # Ermittle Netzwerkschnittstellen + network_interfaces = self.get_network_interfaces() + + return render_template('network_config.html', + config=self.config, + backend_status=self.backend_status, + frontend_status=self.frontend_status, + last_check=self.last_check, + network_interfaces=network_interfaces, + message=request.args.get('message'), + message_type=request.args.get('message_type', 'info')) + + def load_config(self): + """Lädt die gespeicherte Konfiguration.""" + try: + if os.path.exists(self.CONFIG_FILE): + with open(self.CONFIG_FILE, 'r') as f: + saved_config = json.load(f) + self.config.update(saved_config) + self.logger.info(f"Netzwerkkonfiguration geladen: {self.config}") + else: + self.logger.info(f"Keine gespeicherte Konfiguration gefunden, verwende Standardwerte: {self.config}") + except Exception as e: + self.logger.error(f"Fehler beim Laden der Netzwerkkonfiguration: {e}") + + def save_config(self): + """Speichert die aktuelle Konfiguration.""" + try: + with open(self.CONFIG_FILE, 'w') as f: + json.dump(self.config, f, indent=4) + self.logger.info(f"Netzwerkkonfiguration gespeichert: {self.config}") + return True + except Exception as e: + self.logger.error(f"Fehler beim Speichern der Netzwerkkonfiguration: {e}") + return False + + def get_backend_url(self): + """Gibt die Backend-URL zurück.""" + return f"http://{self.config['backend_hostname']}:{self.config['backend_port']}" + + def get_frontend_url(self): + """Gibt die Frontend-URL zurück.""" + return f"http://{self.config['frontend_hostname']}:{self.config['frontend_port']}" + + def check_connection_statuses(self): + """Überprüft den Verbindungsstatus zu Backend und Frontend.""" + self.last_check = datetime.now().strftime("%d.%m.%Y %H:%M:%S") + + # Prüfe Backend-Verbindung + backend_url = self.get_backend_url() + try: + response = requests.get(f"{backend_url}/api/test", timeout=3) + if response.status_code == 200: + self.backend_status = "Verbunden" + else: + self.backend_status = f"Fehler: HTTP {response.status_code}" + except requests.exceptions.RequestException as e: + self.backend_status = f"Nicht erreichbar: {str(e)}" + + # Prüfe Frontend-Verbindung + frontend_url = self.get_frontend_url() + try: + response = requests.get(frontend_url, timeout=3) + if response.status_code == 200: + self.frontend_status = "Verbunden" + else: + self.frontend_status = f"Fehler: HTTP {response.status_code}" + except requests.exceptions.RequestException as e: + self.frontend_status = f"Nicht erreichbar: {str(e)}" + + self.logger.info(f"Verbindungsstatus - Backend: {self.backend_status}, Frontend: {self.frontend_status}") + + def get_network_interfaces(self): + """Gibt Informationen zu allen Netzwerkschnittstellen zurück.""" + interfaces = [] + + try: + for interface in netifaces.interfaces(): + if interface.startswith(('lo', 'docker', 'br-')): + continue # Ignoriere Loopback und Docker-Interfaces + + addresses = [] + try: + addrs = netifaces.ifaddresses(interface) + if netifaces.AF_INET in addrs: + for addr in addrs[netifaces.AF_INET]: + if 'addr' in addr: + addresses.append(addr['addr']) + except Exception as e: + self.logger.error(f"Fehler beim Ermitteln der Adresse für Interface {interface}: {e}") + + if addresses: + interfaces.append({ + 'name': interface, + 'address': ', '.join(addresses) + }) + except Exception as e: + self.logger.error(f"Fehler beim Ermitteln der Netzwerkschnittstellen: {e}") + + return interfaces + +# Helper-Funktion zum Testen von Netzwerkverbindungen +def test_connection(host, port, timeout=2): + """Testet eine TCP-Verbindung zu einem Host und Port.""" + try: + socket.setdefaulttimeout(timeout) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect((host, port)) + s.close() + return True + except Exception as e: + return False + +# Helper-Funktion zum Ping eines Hosts +def ping_host(host, count=1): + """Pingt einen Host an.""" + param = '-n' if platform.system().lower() == 'windows' else '-c' + command = ['ping', param, str(count), host] + return subprocess.call(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0 \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 13666c9a..77531028 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,9 +1,30 @@ +# Core Flask-Abhängigkeiten flask==2.3.3 flask-cors==4.0.0 -pyjwt==2.8.0 -python-dotenv==1.0.0 werkzeug==2.3.7 + +# Authentifizierung und Sicherheit +pyjwt==2.8.0 +flask-wtf==1.1.1 +flask-talisman==1.1.0 + +# Umgebung und Konfiguration +python-dotenv==1.0.0 + +# WSGI-Server für Produktion gunicorn==21.2.0 +waitress==2.1.2 + +# Netzwerk und Hardware-Integration PyP100==0.0.19 netifaces==0.11.0 -requests==2.31.0 \ No newline at end of file +requests==2.31.0 + +# Monitoring und Logging +flask-healthcheck==0.1.0 +prometheus-flask-exporter==0.23.0 + +# Entwicklung und Testing (optional) +pytest==7.4.3 +pytest-flask==1.3.0 +coverage==7.3.2 \ No newline at end of file diff --git a/backend/security.py b/backend/security.py new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/backend/security.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/backend/start-debug-server.bat b/backend/start-debug-server.bat new file mode 100644 index 00000000..f41614a4 --- /dev/null +++ b/backend/start-debug-server.bat @@ -0,0 +1,36 @@ +@echo off +setlocal enabledelayedexpansion + +echo [%date% %time%] Starte Backend-Debug-Server... + +REM Pfad zum Debug-Server ermitteln +set "SCRIPT_DIR=%~dp0" +set "DEBUG_SERVER_DIR=%SCRIPT_DIR%debug-server" + +REM Prüfe, ob das Debug-Server-Verzeichnis existiert +if not exist "%DEBUG_SERVER_DIR%" ( + echo [%date% %time%] FEHLER: Debug-Server-Verzeichnis nicht gefunden: %DEBUG_SERVER_DIR% + exit /b 1 +) + +REM Prüfe, ob Python installiert ist +where python >nul 2>&1 +if %ERRORLEVEL% neq 0 ( + echo [%date% %time%] FEHLER: Python nicht gefunden. Bitte installieren Sie Python. + exit /b 1 +) + +REM Wechsle ins Debug-Server-Verzeichnis +cd "%DEBUG_SERVER_DIR%" + +REM Installiere Abhängigkeiten, falls nötig +if exist "requirements.txt" ( + echo [%date% %time%] Installiere Abhängigkeiten... + pip install -r requirements.txt +) + +REM Starte den Debug-Server +echo [%date% %time%] Starte Backend-Debug-Server... +python app.py + +endlocal \ No newline at end of file diff --git a/backend/start-debug-server.sh b/backend/start-debug-server.sh new file mode 100644 index 00000000..04015db6 --- /dev/null +++ b/backend/start-debug-server.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Farbcodes für Ausgabe +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Funktion zur Ausgabe mit Zeitstempel +log() { + echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" +} + +error_log() { + echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] FEHLER:${NC} $1" >&2 +} + +# Pfad zum Debug-Server +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +DEBUG_SERVER_DIR="$SCRIPT_DIR/debug-server" + +# Prüfe, ob das Debug-Server-Verzeichnis existiert +if [ ! -d "$DEBUG_SERVER_DIR" ]; then + error_log "Debug-Server-Verzeichnis nicht gefunden: $DEBUG_SERVER_DIR" + exit 1 +fi + +# Prüfe, ob Python installiert ist +if ! command -v python &> /dev/null; then + error_log "Python nicht gefunden. Bitte installieren Sie Python." + exit 1 +fi + +# Prüfe, ob pip installiert ist +if ! command -v pip &> /dev/null; then + error_log "pip nicht gefunden. Bitte installieren Sie pip." + exit 1 +fi + +# Wechsle ins Debug-Server-Verzeichnis +cd "$DEBUG_SERVER_DIR" || exit 1 + +# Installiere Abhängigkeiten, falls nötig +if [ -f "requirements.txt" ]; then + log "Installiere Abhängigkeiten..." + pip install -r requirements.txt +fi + +# Starte den Debug-Server +log "Starte Backend-Debug-Server..." +python app.py \ No newline at end of file diff --git a/backend/start-production.bat b/backend/start-production.bat new file mode 100644 index 00000000..d13a7883 --- /dev/null +++ b/backend/start-production.bat @@ -0,0 +1,38 @@ +@echo off +REM MYP Backend - Produktions-Startskript für Windows +REM Startet die Flask-Anwendung mit Waitress für den Produktionsbetrieb + +echo === MYP Backend - Produktionsstart === + +REM Konfiguration +if not defined WORKERS set WORKERS=4 +if not defined BIND_ADDRESS set BIND_ADDRESS=0.0.0.0 +if not defined PORT set PORT=5000 +if not defined THREADS set THREADS=6 + +REM Umgebungsvariablen +set FLASK_ENV=production +set PYTHONPATH=%PYTHONPATH%;%cd% + +REM Log-Verzeichnis erstellen +if not exist logs mkdir logs + +REM Prüfe, ob SECRET_KEY gesetzt ist +if not defined SECRET_KEY ( + echo WARNUNG: SECRET_KEY ist nicht gesetzt. Verwende einen generierten Schlüssel. + for /f %%i in ('python -c "import secrets; print(secrets.token_hex(32))"') do set SECRET_KEY=%%i +) + +REM Produktionsparameter ausgeben +echo Konfiguration: +echo - Host: %BIND_ADDRESS% +echo - Port: %PORT% +echo - Threads: %THREADS% +echo - Environment: %FLASK_ENV% +echo. + +REM Waitress starten (besser für Windows als Gunicorn) +echo Starte Waitress-Server... +python -m waitress --host=%BIND_ADDRESS% --port=%PORT% --threads=%THREADS% --call wsgi:application + +pause \ No newline at end of file diff --git a/backend/start-production.sh b/backend/start-production.sh new file mode 100644 index 00000000..e8f56111 --- /dev/null +++ b/backend/start-production.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# MYP Backend - Produktions-Startskript +# Startet die Flask-Anwendung mit Gunicorn für den Produktionsbetrieb + +set -e + +echo "=== MYP Backend - Produktionsstart ===" + +# Konfiguration +WORKERS=${WORKERS:-4} +BIND_ADDRESS=${BIND_ADDRESS:-"0.0.0.0:5000"} +TIMEOUT=${TIMEOUT:-30} +KEEP_ALIVE=${KEEP_ALIVE:-5} +MAX_REQUESTS=${MAX_REQUESTS:-1000} +MAX_REQUESTS_JITTER=${MAX_REQUESTS_JITTER:-100} + +# Umgebungsvariablen +export FLASK_ENV=production +export PYTHONPATH=${PYTHONPATH}:$(pwd) + +# Log-Verzeichnis erstellen +mkdir -p logs + +# Prüfe, ob alle erforderlichen Umgebungsvariablen gesetzt sind +if [ -z "$SECRET_KEY" ]; then + echo "WARNUNG: SECRET_KEY ist nicht gesetzt. Verwende einen generierten Schlüssel." + export SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))") +fi + +# Produktionsparameter ausgeben +echo "Konfiguration:" +echo " - Workers: $WORKERS" +echo " - Bind: $BIND_ADDRESS" +echo " - Timeout: $TIMEOUT Sekunden" +echo " - Max Requests: $MAX_REQUESTS" +echo " - Environment: $FLASK_ENV" +echo "" + +# Gunicorn starten +echo "Starte Gunicorn-Server..." +exec gunicorn \ + --workers=$WORKERS \ + --worker-class=sync \ + --bind=$BIND_ADDRESS \ + --timeout=$TIMEOUT \ + --keep-alive=$KEEP_ALIVE \ + --max-requests=$MAX_REQUESTS \ + --max-requests-jitter=$MAX_REQUESTS_JITTER \ + --preload \ + --access-logfile=logs/access.log \ + --error-logfile=logs/error.log \ + --log-level=info \ + --capture-output \ + --enable-stdio-inheritance \ + wsgi:application \ No newline at end of file diff --git a/backend/templates/network_config.html b/backend/templates/network_config.html new file mode 100644 index 00000000..1c6aadd6 --- /dev/null +++ b/backend/templates/network_config.html @@ -0,0 +1,119 @@ + + + + + + MYP - Netzwerkkonfiguration + + + +

MYP - Netzwerkkonfiguration

+ + {% if message %} +
+ {{ message }} +
+ {% endif %} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +

Aktuelle Verbindungsstatus

+
+

Backend-Status: {{ backend_status }}

+

Frontend-Status: {{ frontend_status }}

+

Letzte Prüfung: {{ last_check }}

+
+ +

Netzwerkschnittstellen

+
+ {% for interface in network_interfaces %} +

{{ interface.name }}: {{ interface.address }}

+ {% endfor %} +
+ + \ No newline at end of file diff --git a/backend/wsgi.py b/backend/wsgi.py new file mode 100644 index 00000000..5650bdab --- /dev/null +++ b/backend/wsgi.py @@ -0,0 +1,37 @@ +""" +WSGI-Einstiegspunkt für die MYP Flask-Anwendung. +Verwendet für den Produktionsbetrieb mit WSGI-Servern wie Gunicorn. +""" + +import os +from dotenv import load_dotenv + +# Lade Umgebungsvariablen +load_dotenv() + +from app import create_app + +# Erstelle Anwendungsinstanz für Produktionsbetrieb +flask_env = os.environ.get('FLASK_ENV', 'production') +application = create_app(flask_env) + +# Initialisierung für WSGI-Server +with application.app_context(): + from app import init_db, init_printers, setup_frontend_v2 + import json + + # Datenbank initialisieren + init_db() + + # Drucker initialisieren, falls konfiguriert + printers_config = json.loads(application.config.get('PRINTERS', '{}')) + if printers_config: + init_printers() + + # Frontend v2 Setup + setup_frontend_v2() + + application.logger.info(f'MYP Backend gestartet in {flask_env} Modus') + +if __name__ == "__main__": + application.run() \ No newline at end of file diff --git a/cleanup.ps1 b/cleanup.ps1 new file mode 100644 index 00000000..95b5bdc1 --- /dev/null +++ b/cleanup.ps1 @@ -0,0 +1,50 @@ +Write-Host "MYP-Umgebung wird bereinigt..." -ForegroundColor Cyan + +# Stoppen der Debug-Server, falls sie laufen +if (Test-Path -Path "logs\backend-debug.jobid") { + Write-Host "Stoppe Backend Debug-Server..." -ForegroundColor Yellow + $jobId = Get-Content "logs\backend-debug.jobid" + Stop-Job -Id $jobId -ErrorAction SilentlyContinue + Remove-Job -Id $jobId -Force -ErrorAction SilentlyContinue + Remove-Item "logs\backend-debug.jobid" -Force +} + +if (Test-Path -Path "logs\frontend-debug.jobid") { + Write-Host "Stoppe Frontend Debug-Server..." -ForegroundColor Yellow + $jobId = Get-Content "logs\frontend-debug.jobid" + Stop-Job -Id $jobId -ErrorAction SilentlyContinue + Remove-Job -Id $jobId -Force -ErrorAction SilentlyContinue + Remove-Item "logs\frontend-debug.jobid" -Force +} + +# Stoppen und Entfernen aller Docker-Container +Write-Host "Stoppe und entferne alle MYP-Container..." -ForegroundColor Yellow +docker-compose down + +# Entfernen aller MYP-Container, auch die bereits gestoppten +Write-Host "Entferne alle MYP-Container..." -ForegroundColor Yellow +$containers = docker ps -a --filter "name=myp-" -q +if ($containers) { + docker rm -f $containers +} + +# Entfernen aller MYP-Images +Write-Host "Entferne alle MYP-Images..." -ForegroundColor Yellow +$images = docker images --filter "reference=*myp*" -q +if ($images) { + docker rmi -f $images +} + +# Entfernen von nicht verwendeten Volumes (optional) +Write-Host "Entferne nicht verwendete Volumes..." -ForegroundColor Yellow +docker volume prune -f + +# Entfernen von nicht verwendeten Netzwerken (optional) +Write-Host "Entferne nicht verwendete Netzwerke..." -ForegroundColor Yellow +docker network prune -f + +# Entfernen von Build-Cache (optional) +Write-Host "Entferne Docker Build-Cache..." -ForegroundColor Yellow +docker builder prune -f + +Write-Host "Bereinigung abgeschlossen. Sie können nun 'start.ps1' ausführen, um eine frische Installation zu starten." -ForegroundColor Green \ No newline at end of file diff --git a/cleanup.sh b/cleanup.sh new file mode 100644 index 00000000..1539b6a4 --- /dev/null +++ b/cleanup.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +echo "MYP-Umgebung wird bereinigt..." + +# Stoppen der Debug-Server, falls sie laufen +if [ -f logs/backend-debug.pid ]; then + echo "Stoppe Backend Debug-Server..." + kill $(cat logs/backend-debug.pid) 2>/dev/null || true + rm logs/backend-debug.pid +fi + +if [ -f logs/frontend-debug.pid ]; then + echo "Stoppe Frontend Debug-Server..." + kill $(cat logs/frontend-debug.pid) 2>/dev/null || true + rm logs/frontend-debug.pid +fi + +# Stoppen und Entfernen aller Docker-Container +echo "Stoppe und entferne alle MYP-Container..." +docker-compose down + +# Entfernen aller MYP-Container, auch die bereits gestoppten +echo "Entferne alle MYP-Container..." +docker ps -a --filter "name=myp-" -q | xargs -r docker rm -f + +# Entfernen aller MYP-Images +echo "Entferne alle MYP-Images..." +docker images | grep "myp-" | awk '{print $3}' | xargs -r docker rmi -f + +# Entfernen von nicht verwendeten Volumes (optional) +echo "Entferne nicht verwendete Volumes..." +docker volume prune -f + +# Entfernen von nicht verwendeten Netzwerken (optional) +echo "Entferne nicht verwendete Netzwerke..." +docker network prune -f + +# Entfernen von Build-Cache (optional) +echo "Entferne Docker Build-Cache..." +docker builder prune -f + +echo "Bereinigung abgeschlossen. Sie können nun 'start.sh' ausführen, um eine frische Installation zu starten." \ No newline at end of file diff --git a/config/README.md b/config/README.md new file mode 100644 index 00000000..6213b926 --- /dev/null +++ b/config/README.md @@ -0,0 +1,80 @@ +# MYP-Projekt Service-Installation + +Diese Anleitung beschreibt, wie der MYP-Projektservice als systemd-Dienst eingerichtet wird, damit das System beim Booten automatisch startet. + +## Voraussetzungen + +- Docker und Docker Compose sind installiert +- sudo-Rechte auf dem System + +## Installation des Services + +1. Bearbeiten Sie die Datei `myp-service.service` und passen Sie die Pfade an: + + - Ersetzen Sie `/path/to/Projektarbeit-MYP` mit dem tatsächlichen Pfad zum Projektverzeichnis +2. Kopieren Sie die Service-Datei in das systemd-Verzeichnis: + + ```bash + sudo cp myp-service.service /etc/systemd/system/ + ``` +3. Aktualisieren Sie die systemd-Konfiguration: + + ```bash + sudo systemctl daemon-reload + ``` +4. Aktivieren Sie den Service, damit er beim Booten startet: + + ```bash + sudo systemctl enable myp-service + ``` +5. Starten Sie den Service: + + ```bash + sudo systemctl start myp-service + ``` + +## Überprüfen des Service-Status + +Um den Status des Services zu überprüfen: + +```bash +sudo systemctl status myp-service +``` + +## Stoppen des Services + +Um den Service zu stoppen: + +```bash +sudo systemctl stop myp-service +``` + +## Deaktivieren des Autostart + +Um den automatischen Start zu deaktivieren: + +```bash +sudo systemctl disable myp-service +``` + +## Fehlerbehebung + +Überprüfen Sie die Logs bei Problemen: + +```bash +journalctl -u myp-service +``` + +## Hinweis für Windows-Systeme + +Für Windows-Systeme empfehlen wir die Verwendung des Task-Schedulers: + +1. Öffnen Sie den Task-Scheduler (taskschd.msc) +2. Erstellen Sie eine neue Aufgabe: + - Trigger: "Bei Start" + - Aktion: "Programm starten" + - Programm/Skript: `powershell.exe` + - Argumente: `-ExecutionPolicy Bypass -File "C:\Pfad\zu\Projektarbeit-MYP\start.ps1"` + - "Mit höchsten Privilegien ausführen" aktivieren + +Dadurch wird das MYP-Projekt automatisch beim Systemstart gestartet. diff --git a/config/install-linux-service.sh b/config/install-linux-service.sh new file mode 100644 index 00000000..505535a4 --- /dev/null +++ b/config/install-linux-service.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# MYP-Projekt systemd-Service Installationsskript + +# Überprüfen, ob das Skript mit Root-Rechten ausgeführt wird +if [ "$EUID" -ne 0 ]; then + echo "Bitte führen Sie dieses Skript mit Root-Rechten aus (sudo)." + exit 1 +fi + +# Ermitteln des Projektpfads +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +echo "MYP-Projekt Service-Installation" +echo "================================" +echo "Projektpfad: $PROJECT_DIR" + +# Kopieren der Service-Datei mit angepasstem Pfad +echo "Erstelle systemd-Service-Datei..." +cp "$SCRIPT_DIR/myp-service.service" /tmp/myp-service.service +sed -i "s|/path/to/Projektarbeit-MYP|$PROJECT_DIR|g" /tmp/myp-service.service + +# Kopieren der Service-Datei in das systemd-Verzeichnis +echo "Installiere systemd-Service..." +cp /tmp/myp-service.service /etc/systemd/system/ +rm /tmp/myp-service.service + +# Setze Ausführungsrechte für das Start-Skript +chmod +x "$PROJECT_DIR/start.sh" + +# Systemd aktualisieren +echo "Aktualisiere systemd..." +systemctl daemon-reload + +# Service aktivieren +echo "Aktiviere Service für Autostart..." +systemctl enable myp-service + +echo +echo "Installation abgeschlossen." +echo "Möchten Sie den Service jetzt starten? (j/n)" +read -r ANTWORT + +if [[ "$ANTWORT" =~ ^[Jj]$ ]]; then + echo "Starte MYP-Projekt Service..." + systemctl start myp-service + + # Status anzeigen + echo + echo "Service-Status:" + systemctl status myp-service --no-pager +fi + +echo +echo "Sie können den Service-Status jederzeit mit folgendem Befehl überprüfen:" +echo " sudo systemctl status myp-service" +echo +echo "Der MYP-Projekt Service wird nun bei jedem Systemstart automatisch gestartet." \ No newline at end of file diff --git a/config/install-windows-service.bat b/config/install-windows-service.bat new file mode 100644 index 00000000..007291ba --- /dev/null +++ b/config/install-windows-service.bat @@ -0,0 +1,66 @@ +@echo off +echo MYP-Projekt Autostart-Einrichtung +echo ================================= + +REM Erfordert Admin-Rechte +NET SESSION >nul 2>&1 +IF %ERRORLEVEL% NEQ 0 ( + echo Bitte führen Sie dieses Skript mit Administratorrechten aus. + echo Klicken Sie mit der rechten Maustaste und wählen Sie "Als Administrator ausführen". + pause + exit /b 1 +) + +REM Pfad zum Projektverzeichnis ermitteln +set SCRIPT_DIR=%~dp0 +set PROJECT_DIR=%SCRIPT_DIR%.. +cd %PROJECT_DIR% +set PROJECT_PATH=%CD% + +echo. +echo Projektpfad: %PROJECT_PATH% + +REM Erstellung der PowerShell-Skriptdatei für den Task +echo Erstelle PowerShell-Skriptdatei für den Windows Task... +set PS_SCRIPT=%PROJECT_PATH%\config\secure\myp-autostart.ps1 + +if not exist "%PROJECT_PATH%\config\secure" mkdir "%PROJECT_PATH%\config\secure" + +echo $ErrorActionPreference = "Stop" > "%PS_SCRIPT%" +echo try { >> "%PS_SCRIPT%" +echo Write-Host "Starte MYP-Projekt..." >> "%PS_SCRIPT%" +echo Set-Location -Path "%PROJECT_PATH%" >> "%PS_SCRIPT%" +echo Start-Process -FilePath "powershell.exe" -ArgumentList "-ExecutionPolicy Bypass -File '%PROJECT_PATH%\start.ps1'" >> "%PS_SCRIPT%" +echo Write-Host "MYP-Projekt erfolgreich gestartet" >> "%PS_SCRIPT%" +echo } catch { >> "%PS_SCRIPT%" +echo $ErrorMessage = $_.Exception.Message >> "%PS_SCRIPT%" +echo Write-Host "Fehler beim Starten des MYP-Projekts: $ErrorMessage" >> "%PS_SCRIPT%" +echo Add-Content -Path "%PROJECT_PATH%\logs\autostart_error.log" -Value "$(Get-Date) - Fehler: $ErrorMessage" >> "%PS_SCRIPT%" +echo exit 1 >> "%PS_SCRIPT%" +echo } >> "%PS_SCRIPT%" + +REM Erstellung des geplanten Tasks +echo Erstelle geplanten Windows Task... +schtasks /create /tn "MYP-Projekt Autostart" /sc onstart /delay 0000:30 /ru "System" /rl highest /tr "powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File \"%PS_SCRIPT%\"" /f + +if %ERRORLEVEL% NEQ 0 ( + echo Fehler bei der Erstellung des geplanten Tasks. + pause + exit /b 1 +) + +echo. +echo Der MYP-Projekt Autostart wurde erfolgreich eingerichtet. +echo Das System wird nun bei jedem Systemstart automatisch das MYP-Projekt starten. +echo. +echo Möchten Sie das Projekt jetzt starten? +choice /c JN /m "Projekt jetzt starten (J/N)?" + +if %ERRORLEVEL% EQU 1 ( + echo Starte MYP-Projekt... + powershell.exe -ExecutionPolicy Bypass -File "%PROJECT_PATH%\start.ps1" +) + +echo. +echo Installation abgeschlossen. +pause \ No newline at end of file diff --git a/config/myp-service.service b/config/myp-service.service new file mode 100644 index 00000000..873e60a1 --- /dev/null +++ b/config/myp-service.service @@ -0,0 +1,14 @@ +[Unit] +Description=MYP Projektarbeit Service +After=docker.service network.target +Requires=docker.service + +[Service] +Type=oneshot +RemainAfterExit=yes +WorkingDirectory=/path/to/Projektarbeit-MYP +ExecStart=/path/to/Projektarbeit-MYP/start.sh +ExecStop=/usr/bin/docker-compose -f /path/to/Projektarbeit-MYP/docker-compose.yml down + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 00000000..a390a7ff --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,110 @@ +# 🔧 MYP Entwicklungsumgebung - Docker Compose +# Erweiterte Konfiguration für lokale Entwicklung + +version: '3.8' + +services: + # Backend-Entwicklung mit Hot Reload + backend: + build: + context: ./backend + dockerfile: Dockerfile.dev + environment: + - FLASK_ENV=development + - FLASK_DEBUG=true + - PYTHONUNBUFFERED=1 + - WATCHDOG_ENABLED=true + volumes: + - ./backend:/app + - /app/__pycache__ + - backend_logs:/app/logs + ports: + - "5000:5000" + - "5555:5555" # Debug-Server + command: flask run --host=0.0.0.0 --port=5000 --reload + + # Frontend-Entwicklung mit Hot Reload + frontend: + build: + context: ./frontend + dockerfile: Dockerfile.dev + environment: + - NODE_ENV=development + - NEXT_TELEMETRY_DISABLED=1 + - WATCHPACK_POLLING=true + volumes: + - ./frontend:/app + - /app/node_modules + - /app/.next + ports: + - "3000:3000" + - "8081:8081" # Debug-Server + command: pnpm dev + + # Monitoring Services + prometheus: + image: prom/prometheus:latest + container_name: myp-prometheus + restart: unless-stopped + ports: + - "9090:9090" + volumes: + - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus_data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--web.console.libraries=/etc/prometheus/console_libraries' + - '--web.console.templates=/etc/prometheus/consoles' + - '--storage.tsdb.retention.time=200h' + - '--web.enable-lifecycle' + networks: + - myp-network + + grafana: + image: grafana/grafana:latest + container_name: myp-grafana + restart: unless-stopped + ports: + - "3001:3000" + volumes: + - grafana_data:/var/lib/grafana + - ./monitoring/grafana/provisioning:/etc/grafana/provisioning + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_USERS_ALLOW_SIGN_UP=false + networks: + - myp-network + + # Datenbank-Viewer (Adminer) + adminer: + image: adminer:latest + container_name: myp-adminer + restart: unless-stopped + ports: + - "8080:8080" + networks: + - myp-network + + # Redis für Caching (optional) + redis: + image: redis:7-alpine + container_name: myp-redis + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis_data:/data + networks: + - myp-network + +volumes: + backend_logs: + prometheus_data: + grafana_data: + redis_data: + +networks: + myp-network: + external: true \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..c1f07e57 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,95 @@ +version: '3' + +services: + # Backend + backend: + build: + context: ./backend + container_name: myp-backend + restart: always + environment: + - SECRET_KEY=${SECRET_KEY:-7445630171969DFAC92C53CEC92E67A9CB2E00B3CB2F} + - DATABASE_PATH=${DATABASE_PATH:-instance/myp.db} + - TAPO_USERNAME=${TAPO_USERNAME:-till.tomczak@mercedes-benz.com} + - TAPO_PASSWORD=${TAPO_PASSWORD:-744563017196A} + - "PRINTERS=${PRINTERS:-{\"Printer 1\": {\"ip\": \"192.168.0.100\"}, \"Printer 2\": {\"ip\": \"192.168.0.101\"}, \"Printer 3\": {\"ip\": \"192.168.0.102\"}, \"Printer 4\": {\"ip\": \"192.168.0.103\"}, \"Printer 5\": {\"ip\": \"192.168.0.104\"}, \"Printer 6\": {\"ip\": \"192.168.0.106\"}}}" + - FLASK_APP=app.py + - PYTHONUNBUFFERED=1 + - HOST=0.0.0.0 + - PORT=5000 + volumes: + - ./backend/logs:/app/logs + - ./backend/instance:/app/instance + networks: + myp-network: + ipv4_address: 192.168.0.5 + expose: + - "5000" + ports: + - "5000:5000" + healthcheck: + test: ["CMD", "wget", "--spider", "http://localhost:5000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # Next.js Frontend + frontend: + build: + context: ./frontend + container_name: myp-rp + restart: unless-stopped + environment: + - NODE_ENV=production + - NEXT_PUBLIC_API_URL=/api + networks: + - myp-network + expose: + - "3000" + healthcheck: + test: ["CMD", "wget", "--spider", "http://localhost:3000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + depends_on: + - backend + + # Caddy Proxy + caddy: + image: caddy:2.7-alpine + container_name: myp-caddy + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./frontend/docker/caddy/Caddyfile:/etc/caddy/Caddyfile + - caddy_data:/data + - caddy_config:/config + networks: + - myp-network + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + - CADDY_HOST=53.37.211.254 + - CADDY_DOMAIN=m040tbaraspi001.de040.corpintra.net + cap_add: + - NET_ADMIN + depends_on: + - frontend + - backend + +networks: + myp-network: + driver: bridge + ipam: + driver: default + config: + - subnet: 192.168.0.0/24 + gateway: 192.168.0.1 + +volumes: + caddy_data: + caddy_config: \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 00000000..70020991 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,43 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# db folder +db/ + +# Env file +.env + + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 00000000..11ed8ada --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,34 @@ +FROM node:20-bookworm-slim + +# Create application directory +RUN mkdir -p /usr/src/app + +# Set environment variables +ENV PORT=3000 +ENV NEXT_TELEMETRY_DISABLED=1 + +WORKDIR /usr/src/app + +# Copy package.json and pnpm-lock.yaml +COPY package.json /usr/src/app +COPY pnpm-lock.yaml /usr/src/app + +# Install pnpm +RUN corepack enable pnpm + +# Install dependencies +RUN pnpm install + +# Copy the rest of the application code +COPY . /usr/src/app + +# Initialize Database, if it not already exists +RUN pnpm run db + +# Build the application +RUN pnpm run build + +EXPOSE 3000 + +# Start the application +CMD ["/bin/sh", "-c", "if [ ! -f ./db/sqlite.db ]; then pnpm db; fi && pnpm start"] diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..8faebf07 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,32 @@ +# MYP - Manage Your Printer + +MYP (Manage Your Printer) ist eine Webanwendung zur Reservierung von 3D-Druckern. +Sie wurde im Rahmen des Abschlussprojektes der Fachinformatiker Ausbildung für Daten- und Prozessanalyse für die Technische Berufsausbildung des Mercedes-Benz Werkes Berlin-Marienfelde entwickelt. + +## Deployment + +### Voraussetzungen + +- Netzwerk auf Raspberry Pi ist eingerichtet +- Docker ist installiert + +### Schritte + +1. Docker-Container bauen (docker/build.sh) +2. Docker-Container speichern (docker/save.sh caddy:2.8 myp-rp:latest) +3. Docker-Container auf Raspberry Pi bereitstellen (docker/deploy.sh) + +## Entwicklerinformationen + +### Raspberry Pi Einstellungen + +Auf dem Raspberry Pi wurde Raspbian Lite installiert. +Unter /srv/* sind die Projektdateien zu finden. + +### Anmeldedaten + +``` +Benutzer: myp +Passwort: (persönlich bekannt) +``` + diff --git a/frontend/biome.json b/frontend/biome.json new file mode 100644 index 00000000..4a0b3f42 --- /dev/null +++ b/frontend/biome.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.7.0/schema.json", + "organizeImports": { + "enabled": true + }, + "formatter": { + "enabled": true, + "lineWidth": 120 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "correctness": { + "noUnusedImports": "error" + } + } + } +} \ No newline at end of file diff --git a/frontend/cleanup.sh b/frontend/cleanup.sh new file mode 100644 index 00000000..e2f5d3b9 --- /dev/null +++ b/frontend/cleanup.sh @@ -0,0 +1,245 @@ +#!/bin/bash + +# Raspberry Pi Bereinigungsskript für MYP-Projekt Frontend +# Dieses Skript bereinigt alte Docker-Installationen und installiert alle erforderlichen Abhängigkeiten + +# Farbcodes für Ausgabe +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Funktion zur Ausgabe mit Zeitstempel +log() { + echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" +} + +error_log() { + echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] FEHLER:${NC} $1" >&2 +} + +# Prüfen, ob das Skript mit Root-Rechten ausgeführt wird +if [ "$EUID" -ne 0 ]; then + error_log "Dieses Skript muss mit Root-Rechten ausgeführt werden (sudo)." + exit 1 +fi + +log "${YELLOW}=== MYP Frontend Raspberry Pi Bereinigung und Setup ===${NC}" +log "Dieses Skript wird alle alten Docker-Installationen entfernen und die erforderlichen Abhängigkeiten neu installieren." + +# Sicherstellen, dass apt funktioniert +log "Aktualisiere apt-Paketindex..." +apt-get update || { + error_log "Konnte apt-Paketindex nicht aktualisieren." + exit 1 +} + +# Installiere grundlegende Abhängigkeiten +log "Installiere grundlegende Abhängigkeiten..." +apt-get install -y \ + apt-transport-https \ + ca-certificates \ + curl \ + gnupg \ + lsb-release \ + wget \ + git \ + jq \ + || { + error_log "Konnte grundlegende Abhängigkeiten nicht installieren." + exit 1 +} + +# Stoppe alle laufenden Docker-Container +log "${YELLOW}Stoppe alle laufenden Docker-Container...${NC}" +if command -v docker &> /dev/null; then + docker stop $(docker ps -aq) 2>/dev/null || true + log "Alle Docker-Container gestoppt." +else + log "Docker ist nicht installiert, keine Container zu stoppen." +fi + +# Entferne alte Docker-Installation +log "${YELLOW}Entferne alte Docker-Installation...${NC}" +apt-get remove -y docker docker-engine docker.io containerd runc docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-compose || true +apt-get autoremove -y || true +rm -rf /var/lib/docker /var/lib/containerd /var/run/docker.sock /etc/docker /usr/local/bin/docker-compose 2>/dev/null || true +log "${GREEN}Alte Docker-Installation entfernt.${NC}" + +# Entferne alte Projektcontainer und -Dateien +log "${YELLOW}Entferne alte MYP-Projektcontainer und -Dateien...${NC}" +if command -v docker &> /dev/null; then + # Entferne Container + docker rm -f myp-frontend myp-backend 2>/dev/null || true + # Entferne Images + docker rmi -f myp-frontend myp-backend 2>/dev/null || true + # Entferne unbenutzte Volumes und Netzwerke + docker system prune -af --volumes 2>/dev/null || true +fi + +# Erkennen der Raspberry Pi-Architektur +log "Erkenne Systemarchitektur..." +ARCH=$(dpkg --print-architecture) +log "Erkannte Architektur: ${ARCH}" + +# Installiere Docker mit dem offiziellen Convenience-Skript +log "${YELLOW}Installiere Docker mit dem offiziellen Convenience-Skript...${NC}" +curl -fsSL https://get.docker.com -o get-docker.sh +sh get-docker.sh --channel stable + +# Überprüfen, ob Docker erfolgreich installiert wurde +if ! command -v docker &> /dev/null; then + error_log "Docker-Installation fehlgeschlagen!" + exit 1 +fi + +log "${GREEN}Docker erfolgreich installiert!${NC}" + +# Füge den aktuellen Benutzer zur Docker-Gruppe hinzu +if [ "$SUDO_USER" ]; then + log "Füge Benutzer $SUDO_USER zur Docker-Gruppe hinzu..." + usermod -aG docker $SUDO_USER + log "${YELLOW}Hinweis: Eine Neuanmeldung ist erforderlich, damit die Gruppenänderung wirksam wird.${NC}" +fi + +# Konfiguriere Docker mit DNS-Servern für bessere Netzwerkkompatibilität +log "Konfiguriere Docker mit Google DNS..." +mkdir -p /etc/docker +cat > /etc/docker/daemon.json << EOL +{ + "dns": ["8.8.8.8", "8.8.4.4"] +} +EOL + +# Starte Docker-Dienst neu +log "Starte Docker-Dienst neu..." +systemctl restart docker +systemctl enable docker +log "${GREEN}Docker-Dienst neu gestartet und für den Autostart aktiviert.${NC}" + +# Installiere Docker Compose v2 +log "${YELLOW}Installiere Docker Compose...${NC}" + +# Bestimme die passende Docker Compose-Version für die Architektur +if [ "$ARCH" = "armhf" ]; then + log "Installiere Docker Compose für armhf (32-bit)..." + curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-armv7" -o /usr/local/bin/docker-compose +elif [ "$ARCH" = "arm64" ]; then + log "Installiere Docker Compose für arm64 (64-bit)..." + curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-linux-aarch64" -o /usr/local/bin/docker-compose +else + log "Unbekannte Architektur, verwende automatische Erkennung..." + curl -L "https://github.com/docker/compose/releases/download/v2.6.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +fi + +chmod +x /usr/local/bin/docker-compose + +# Überprüfe, ob Docker Compose installiert wurde +if ! command -v docker-compose &> /dev/null; then + error_log "Docker Compose-Installation fehlgeschlagen!" + exit 1 +fi + +# Installiere Docker Compose Plugin (neuere Methode) +log "Installiere Docker Compose Plugin..." +apt-get update +apt-get install -y docker-compose-plugin + +log "${GREEN}Docker Compose erfolgreich installiert!${NC}" +docker compose version || docker-compose --version + +# Installiere zusätzliche Abhängigkeiten für das Frontend +log "${YELLOW}Installiere zusätzliche Frontend-Abhängigkeiten...${NC}" +apt-get install -y \ + nodejs \ + npm \ + || { + error_log "Konnte zusätzliche Frontend-Abhängigkeiten nicht installieren." + exit 1 +} + +# Installiere Node.js LTS-Version über NodeSource +log "${YELLOW}Installiere aktuelle Node.js LTS-Version...${NC}" +curl -fsSL https://deb.nodesource.com/setup_18.x | bash - +apt-get install -y nodejs + +# Überprüfe Node.js-Installation +NODE_VERSION=$(node -v) +log "${GREEN}Node.js $NODE_VERSION erfolgreich installiert!${NC}" + +# Installiere pnpm +log "${YELLOW}Installiere pnpm Paketmanager...${NC}" +npm install -g pnpm +log "${GREEN}pnpm $(pnpm --version) erfolgreich installiert!${NC}" + +# Optimieren des Raspberry Pi für Docker-Workloads +log "${YELLOW}Optimiere Raspberry Pi für Docker-Workloads...${NC}" + +# Swap erhöhen für bessere Performance bei begrenztem RAM +log "Konfiguriere Swap-Größe..." +CURRENT_SWAP=$(grep "CONF_SWAPSIZE" /etc/dphys-swapfile | cut -d= -f2) +log "Aktuelle Swap-Größe: ${CURRENT_SWAP}" + +# Erhöhe Swap auf 2GB, wenn weniger +if [ "$CURRENT_SWAP" -lt 2048 ]; then + sed -i 's/^CONF_SWAPSIZE=.*/CONF_SWAPSIZE=2048/' /etc/dphys-swapfile + log "Swap-Größe auf 2048MB erhöht, Neustart des Swap-Dienstes erforderlich." + + # Neustart des Swap-Dienstes + /etc/init.d/dphys-swapfile restart +else + log "Swap-Größe ist bereits ausreichend." +fi + +# Konfiguriere cgroup für Docker +if ! grep -q "cgroup_enable=memory" /boot/cmdline.txt; then + log "Konfiguriere cgroup für Docker..." + CMDLINE=$(cat /boot/cmdline.txt) + echo "$CMDLINE cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1" > /boot/cmdline.txt + log "${YELLOW}WICHTIG: Ein Systemneustart ist erforderlich, damit die cgroup-Änderungen wirksam werden.${NC}" +fi + +# Erstelle Datenbankverzeichnis für Frontend +log "Erstelle Datenbankverzeichnis für Frontend..." +mkdir -p /srv/MYP-DB +chmod 777 /srv/MYP-DB + +# Erstelle Umgebungsvariablen-Verzeichnis +log "Erstelle Umgebungsvariablen-Verzeichnis..." +mkdir -p /srv/myp-env +cat > /srv/myp-env/github.env << EOL +# OAuth-Konfiguration für Frontend +OAUTH_CLIENT_ID=client_id +OAUTH_CLIENT_SECRET=client_secret +NEXT_PUBLIC_API_URL=http://192.168.0.105:5000 +EOL +chmod 600 /srv/myp-env/github.env + +# Prüfe, ob Frontend-Installationsdateien vorhanden sind +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +FRONTEND_DIR="$SCRIPT_DIR" + +if [ -f "$FRONTEND_DIR/package.json" ]; then + log "${GREEN}Frontend-Projektdateien gefunden in $FRONTEND_DIR${NC}" +else + log "${YELLOW}Warnung: Frontend-Projektdateien nicht gefunden in $FRONTEND_DIR${NC}" +fi + +# Abschlussmeldung +log "${GREEN}=== Bereinigung und Setup abgeschlossen ===${NC}" +log "${YELLOW}WICHTIGE HINWEISE:${NC}" +log "1. Ein ${RED}SYSTEMNEUSTART${NC} ist ${RED}DRINGEND ERFORDERLICH${NC}, damit alle Änderungen wirksam werden." +log "2. Nach dem Neustart können Sie das Frontend-Installationsskript ausführen:" +log " cd $FRONTEND_DIR && ./install.sh" +log "3. Bei Problemen mit Docker-Berechtigungen stellen Sie sicher, dass Sie sich neu angemeldet haben." +log "4. Sie müssen noch die OAuth-Konfiguration in /srv/myp-env/github.env anpassen." + +echo "" +read -p "Möchten Sie das System jetzt neu starten? (j/n): " REBOOT_CHOICE +if [[ "$REBOOT_CHOICE" == "j" ]]; then + log "System wird neu gestartet..." + reboot +else + log "Bitte starten Sie das System manuell neu, bevor Sie die Installationsskripte ausführen." +fi \ No newline at end of file diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 00000000..a158e943 --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/utils/styles" + } +} \ No newline at end of file diff --git a/frontend/debug-server/package.json b/frontend/debug-server/package.json new file mode 100644 index 00000000..c34edc11 --- /dev/null +++ b/frontend/debug-server/package.json @@ -0,0 +1,18 @@ +{ + "name": "myp-frontend-debug-server", + "version": "1.0.0", + "description": "Debug-Server für das MYP Frontend", + "main": "src/app.js", + "scripts": { + "start": "node src/app.js", + "dev": "nodemon src/app.js" + }, + "dependencies": { + "axios": "^1.6.2", + "ejs": "^3.1.9", + "express": "^4.18.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} \ No newline at end of file diff --git a/frontend/debug-server/public/css/style.css b/frontend/debug-server/public/css/style.css new file mode 100644 index 00000000..61ee043e --- /dev/null +++ b/frontend/debug-server/public/css/style.css @@ -0,0 +1,417 @@ +/* Variablen */ +:root { + --primary-color: #3f51b5; + --secondary-color: #283593; + --accent-color: #ff4081; + --background-color: #f5f5f5; + --card-color: #ffffff; + --text-color: #333333; + --text-light: #757575; + --border-color: #dddddd; + --success-color: #4caf50; + --warning-color: #ff9800; + --danger-color: #f44336; + --shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + --card-shadow: 0 3px 6px rgba(0, 0, 0, 0.16); +} + +/* Grundlegende Stile */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + line-height: 1.6; + color: var(--text-color); + background-color: var(--background-color); + display: flex; + flex-direction: column; + min-height: 100vh; +} + +/* Header */ +header { + background-color: var(--primary-color); + color: white; + padding: 1rem 2rem; + box-shadow: var(--shadow); + position: sticky; + top: 0; + z-index: 100; +} + +.header-content { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + max-width: 1400px; + margin: 0 auto; +} + +h1 { + font-size: 1.8rem; + margin-bottom: 0.5rem; +} + +.server-info { + display: flex; + flex-wrap: wrap; + gap: 1rem; + font-size: 0.9rem; +} + +/* Navigation */ +nav { + background-color: var(--secondary-color); + display: flex; + justify-content: center; + padding: 0.5rem 1rem; + overflow-x: auto; + white-space: nowrap; + position: sticky; + top: 0; + z-index: 99; + box-shadow: var(--shadow); +} + +.nav-button { + background: none; + color: rgba(255, 255, 255, 0.8); + border: none; + padding: 0.75rem 1.25rem; + margin: 0 0.25rem; + cursor: pointer; + font-size: 1rem; + border-radius: 4px; + transition: all 0.3s; +} + +.nav-button:hover { + color: white; + background-color: rgba(255, 255, 255, 0.1); +} + +.nav-button.active { + color: white; + background-color: var(--primary-color); + font-weight: bold; +} + +/* Main Content */ +main { + flex: 1; + padding: 2rem; + max-width: 1400px; + margin: 0 auto; + width: 100%; +} + +.panel { + display: none; +} + +.panel.active { + display: block; + animation: fadeIn 0.3s; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +h2 { + margin-bottom: 1.5rem; + color: var(--secondary-color); + border-bottom: 2px solid var(--primary-color); + padding-bottom: 0.5rem; +} + +.card-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1.5rem; +} + +.card { + background-color: var(--card-color); + border-radius: 8px; + padding: 1.5rem; + box-shadow: var(--card-shadow); + transition: transform 0.3s, box-shadow 0.3s; +} + +.card:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); +} + +.card.full-width { + grid-column: 1 / -1; +} + +h3 { + margin-bottom: 1rem; + color: var(--primary-color); + font-size: 1.2rem; +} + +/* Loader */ +.loader { + text-align: center; + margin: 2rem 0; + color: var(--text-light); + font-style: italic; +} + +/* Fortschrittsbalken */ +.progress-container { + margin-top: 1rem; + background-color: #e0e0e0; + border-radius: 4px; + height: 8px; + overflow: hidden; +} + +.progress-bar { + height: 100%; + background-color: var(--primary-color); + width: 0%; + transition: width 0.5s ease-in-out; +} + +/* Tool-Karten */ +.tool-card { + display: flex; + flex-direction: column; + height: 100%; +} + +.tool-input { + display: flex; + margin-bottom: 1rem; + gap: 0.5rem; +} + +.tool-input input { + flex: 1; + padding: 0.5rem; + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 1rem; +} + +.tool-input button { + background-color: var(--primary-color); + color: white; + border: none; + padding: 0.5rem 1rem; + cursor: pointer; + border-radius: 4px; + font-size: 1rem; + transition: background-color 0.3s; +} + +.tool-input button:hover { + background-color: var(--secondary-color); +} + +.result-box { + background-color: #f8f9fa; + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 1rem; + overflow: auto; + min-height: 150px; + max-height: 300px; + font-family: 'Consolas', 'Courier New', monospace; + font-size: 0.9rem; + flex: 1; +} + +/* Tabellen */ +table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; +} + +th, td { + padding: 0.75rem; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +th { + background-color: rgba(0, 0, 0, 0.05); + font-weight: bold; + color: var(--text-color); +} + +tr:hover { + background-color: rgba(0, 0, 0, 0.02); +} + +/* Status-Anzeigen */ +.status { + display: inline-block; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.9rem; + font-weight: bold; +} + +.status-online { + background-color: var(--success-color); + color: white; +} + +.status-offline { + background-color: var(--danger-color); + color: white; +} + +.status-warning { + background-color: var(--warning-color); + color: white; +} + +/* Echtzeit-Monitor */ +.gauge-container { + display: flex; + justify-content: center; + margin: 1rem 0; +} + +.gauge { + position: relative; + width: 150px; + height: 75px; + margin: 0 auto; +} + +.gauge-body { + position: relative; + width: 100%; + height: 100%; + border-top-left-radius: 100px; + border-top-right-radius: 100px; + background-color: #e0e0e0; + overflow: hidden; +} + +.gauge-fill { + position: absolute; + bottom: 0; + width: 100%; + height: 0%; + background-color: var(--primary-color); + transition: height 0.5s; +} + +.gauge-cover { + position: absolute; + bottom: 0; + width: 90%; + height: 90%; + margin-left: 5%; + margin-bottom: 5%; + background-color: var(--card-color); + border-top-left-radius: 80px; + border-top-right-radius: 80px; +} + +.gauge-value { + position: absolute; + bottom: -25px; + width: 100%; + text-align: center; + font-size: 1.5rem; + font-weight: bold; + color: var(--primary-color); +} + +/* CPU-Cores */ +.cpu-core { + margin: 0.5rem 0; +} + +.cpu-core-label { + display: flex; + justify-content: space-between; + margin-bottom: 0.25rem; +} + +.cpu-core-bar { + height: 6px; + background-color: #e0e0e0; + border-radius: 3px; +} + +.cpu-core-fill { + height: 100%; + background-color: var(--primary-color); + border-radius: 3px; + transition: width 0.5s; +} + +/* Network Chart */ +canvas { + margin-top: 1rem; + width: 100%; + height: 200px; +} + +/* Footer */ +footer { + background-color: var(--secondary-color); + color: white; + text-align: center; + padding: 1rem; + margin-top: auto; + font-size: 0.9rem; +} + +footer p { + margin: 0.25rem 0; +} + +/* Responsive Design */ +@media (max-width: 768px) { + header { + padding: 1rem; + } + + .header-content { + flex-direction: column; + align-items: flex-start; + } + + .server-info { + margin-top: 1rem; + flex-direction: column; + gap: 0.5rem; + } + + main { + padding: 1rem; + } + + .card-container { + grid-template-columns: 1fr; + } + + .nav-button { + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + } + + .gauge { + width: 120px; + height: 60px; + } +} \ No newline at end of file diff --git a/frontend/debug-server/public/js/script.js b/frontend/debug-server/public/js/script.js new file mode 100644 index 00000000..8586a033 --- /dev/null +++ b/frontend/debug-server/public/js/script.js @@ -0,0 +1,505 @@ +// DOM-Element-Referenzen +const navButtons = document.querySelectorAll('.nav-button'); +const panels = document.querySelectorAll('.panel'); + +// Panel-Navigation +navButtons.forEach(button => { + button.addEventListener('click', () => { + const panelId = button.getAttribute('data-panel'); + + // Aktiven Button und Panel wechseln + navButtons.forEach(btn => btn.classList.remove('active')); + panels.forEach(panel => panel.classList.remove('active')); + + button.classList.add('active'); + document.getElementById(panelId).classList.add('active'); + + // Lade Panel-Daten, wenn sie noch nicht geladen wurden + if (panelId === 'system' && document.getElementById('system-container').style.display === 'none') { + loadSystemInfo(); + } else if (panelId === 'network' && document.getElementById('network-container').style.display === 'none') { + loadNetworkInfo(); + } else if (panelId === 'services' && document.getElementById('services-container').style.display === 'none') { + loadServicesInfo(); + } + }); +}); + +// API-Anfragen +async function fetchData(endpoint) { + try { + const response = await fetch(endpoint); + if (!response.ok) { + throw new Error(`HTTP-Fehler: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error(`Fehler beim Abrufen von ${endpoint}:`, error); + return null; + } +} + +// System-Informationen laden +async function loadSystemInfo() { + const loader = document.getElementById('system-loader'); + const container = document.getElementById('system-container'); + + loader.style.display = 'block'; + container.style.display = 'none'; + + const data = await fetchData('/api/system'); + if (!data) { + loader.textContent = 'Fehler beim Laden der Systemdaten'; + return; + } + + // Betriebssystem-Info + document.getElementById('os-info').innerHTML = ` +

Plattform: ${data.os.platform}

+

Distribution: ${data.os.distro}

+

Version: ${data.os.release}

+

Architektur: ${data.os.arch}

+

Betriebszeit: ${data.os.uptime}

+ `; + + // CPU-Info + document.getElementById('cpu-info').innerHTML = ` +

Hersteller: ${data.cpu.manufacturer}

+

Modell: ${data.cpu.brand}

+

Geschwindigkeit: ${data.cpu.speed} GHz

+

Kerne: ${data.cpu.cores} (${data.cpu.physicalCores} physisch)

+ `; + + // Speicher-Info + document.getElementById('memory-info').innerHTML = ` +

Gesamt: ${data.memory.total}

+

Verwendet: ${data.memory.used} (${data.memory.usedPercent}%)

+

Frei: ${data.memory.free}

+ `; + + document.getElementById('memory-bar').style.width = `${data.memory.usedPercent}%`; + document.getElementById('memory-bar').style.backgroundColor = getColorByPercentage(data.memory.usedPercent); + + // Festplatten-Info + let diskHTML = ''; + + data.filesystem.forEach(fs => { + diskHTML += ` + + + + + + + + + `; + }); + + diskHTML += '
LaufwerkTypGrößeVerwendetVerfügbarNutzung
${fs.mount}${fs.type}${fs.size}${fs.used}${fs.available} +
+
+
+ ${fs.usePercent}% +
'; + document.getElementById('disk-info').innerHTML = diskHTML; + + // UI anzeigen + loader.style.display = 'none'; + container.style.display = 'grid'; +} + +// Netzwerk-Informationen laden +async function loadNetworkInfo() { + const loader = document.getElementById('network-loader'); + const container = document.getElementById('network-container'); + + loader.style.display = 'block'; + container.style.display = 'none'; + + const data = await fetchData('/api/network'); + if (!data) { + loader.textContent = 'Fehler beim Laden der Netzwerkdaten'; + return; + } + + // Netzwerkschnittstellen + let interfacesHTML = ''; + + data.interfaces.forEach(iface => { + interfacesHTML += ` + + + + + + + + + + `; + }); + + interfacesHTML += '
NameStatusIPv4IPv6MACTypDHCP
${iface.iface}${iface.operstate}${iface.ip4 || '-'}${iface.ip6 || '-'}${iface.mac || '-'}${iface.type || '-'}${iface.dhcp ? 'Ja' : 'Nein'}
'; + document.getElementById('network-interfaces').innerHTML = interfacesHTML; + + // DNS-Server + let dnsHTML = '
    '; + data.dns.forEach(server => { + dnsHTML += `
  • ${server}
  • `; + }); + dnsHTML += '
'; + document.getElementById('dns-servers').innerHTML = dnsHTML; + + // Gateway + document.getElementById('default-gateway').innerHTML = `

${data.gateway || 'Nicht verfügbar'}

`; + + // Netzwerkstatistiken + let statsHTML = ''; + + data.stats.forEach(stat => { + statsHTML += ` + + + + + + + + `; + }); + + statsHTML += '
SchnittstelleEmpfangenGesendetEmpfangen/sGesendet/s
${stat.iface}${stat.rx_bytes}${stat.tx_bytes}${stat.rx_sec}${stat.tx_sec}
'; + document.getElementById('network-stats').innerHTML = statsHTML; + + // UI anzeigen + loader.style.display = 'none'; + container.style.display = 'grid'; +} + +// Dienst-Informationen laden +async function loadServicesInfo() { + const loader = document.getElementById('services-loader'); + const container = document.getElementById('services-container'); + + loader.style.display = 'block'; + container.style.display = 'none'; + + const data = await fetchData('/api/services'); + if (!data) { + loader.textContent = 'Fehler beim Laden der Dienstdaten'; + return; + } + + // Frontend-Status + document.getElementById('frontend-status').innerHTML = ` +

Status: ${data.frontend.status}

+

Name: ${data.frontend.name}

+

Host: ${data.frontend.host}

+

Port: ${data.frontend.port}

+ `; + + // Backend-Status + document.getElementById('backend-status').innerHTML = ` +

Status: ${data.backend.status}

+

Name: ${data.backend.name}

+

Host: ${data.backend.host}

+

Port: ${data.backend.port}

+ `; + + // Docker-Container + if (data.docker.containers && data.docker.containers.length > 0) { + let containersHTML = ''; + + data.docker.containers.forEach(container => { + containersHTML += ` + + + + + + + + `; + }); + + containersHTML += '
IDNameImageStatusPorts
${container.id}${container.name}${container.image}${container.status}${container.ports}
'; + document.getElementById('docker-container-list').innerHTML = containersHTML; + } else { + document.getElementById('docker-container-list').innerHTML = '

Keine Docker-Container gefunden

'; + } + + // UI anzeigen + loader.style.display = 'none'; + container.style.display = 'grid'; +} + +// Netzwerk-Tools +document.getElementById('ping-button').addEventListener('click', async () => { + const hostInput = document.getElementById('ping-host'); + const resultBox = document.getElementById('ping-result'); + const host = hostInput.value.trim(); + + if (!host) { + resultBox.textContent = 'Bitte geben Sie einen Hostnamen oder eine IP-Adresse ein'; + return; + } + + resultBox.textContent = 'Ping wird ausgeführt...'; + + const data = await fetchData(`/api/ping/${encodeURIComponent(host)}`); + if (!data) { + resultBox.textContent = 'Fehler beim Ausführen des Ping-Befehls'; + return; + } + + resultBox.textContent = data.output || data.error || 'Keine Ausgabe'; +}); + +document.getElementById('traceroute-button').addEventListener('click', async () => { + const hostInput = document.getElementById('traceroute-host'); + const resultBox = document.getElementById('traceroute-result'); + const host = hostInput.value.trim(); + + if (!host) { + resultBox.textContent = 'Bitte geben Sie einen Hostnamen oder eine IP-Adresse ein'; + return; + } + + resultBox.textContent = 'Traceroute wird ausgeführt...'; + + const data = await fetchData(`/api/traceroute/${encodeURIComponent(host)}`); + if (!data) { + resultBox.textContent = 'Fehler beim Ausführen des Traceroute-Befehls'; + return; + } + + resultBox.textContent = data.output || data.error || 'Keine Ausgabe'; +}); + +document.getElementById('nslookup-button').addEventListener('click', async () => { + const hostInput = document.getElementById('nslookup-host'); + const resultBox = document.getElementById('nslookup-result'); + const host = hostInput.value.trim(); + + if (!host) { + resultBox.textContent = 'Bitte geben Sie einen Hostnamen oder eine IP-Adresse ein'; + return; + } + + resultBox.textContent = 'DNS-Abfrage wird ausgeführt...'; + + const data = await fetchData(`/api/nslookup/${encodeURIComponent(host)}`); + if (!data) { + resultBox.textContent = 'Fehler beim Ausführen des NSLookup-Befehls'; + return; + } + + resultBox.textContent = data.output || data.error || 'Keine Ausgabe'; +}); + +// Echtzeit-Monitoring mit WebSockets +const socket = io(); + +// CPU- und RAM-Statistiken +socket.on('system-stats', data => { + // CPU-Anzeige aktualisieren + const cpuPercentage = data.cpu.load; + document.getElementById('cpu-percentage').textContent = `${cpuPercentage}%`; + document.getElementById('cpu-gauge').style.height = `${cpuPercentage}%`; + document.getElementById('cpu-gauge').style.backgroundColor = getColorByPercentage(cpuPercentage); + + // CPU-Kerne anzeigen + const cpuCoresContainer = document.getElementById('cpu-cores-container'); + + if (cpuCoresContainer.childElementCount === 0) { + // Erstelle Kern-Anzeigen, falls sie noch nicht existieren + data.cpu.cores.forEach((load, index) => { + const coreElement = document.createElement('div'); + coreElement.className = 'cpu-core'; + coreElement.innerHTML = ` +
+ Kern ${index} + ${load}% +
+
+
+
+ `; + cpuCoresContainer.appendChild(coreElement); + }); + } else { + // Aktualisiere bestehende Kern-Anzeigen + data.cpu.cores.forEach((load, index) => { + const valueElement = document.getElementById(`cpu-core-${index}-value`); + const fillElement = document.getElementById(`cpu-core-${index}-fill`); + + if (valueElement && fillElement) { + valueElement.textContent = `${load}%`; + fillElement.style.width = `${load}%`; + fillElement.style.backgroundColor = getColorByPercentage(load); + } + }); + } + + // RAM-Anzeige aktualisieren + const memPercentage = data.memory.usedPercent; + document.getElementById('memory-percentage').textContent = `${memPercentage}%`; + document.getElementById('memory-gauge').style.height = `${memPercentage}%`; + document.getElementById('memory-gauge').style.backgroundColor = getColorByPercentage(memPercentage); + + document.getElementById('memory-details').innerHTML = ` +

Gesamt: ${formatBytes(data.memory.total)}

+

Verwendet: ${formatBytes(data.memory.used)}

+

Frei: ${formatBytes(data.memory.free)}

+ `; +}); + +// Netzwerkstatistiken +let networkChart; +socket.on('network-stats', data => { + const networkThroughput = document.getElementById('network-throughput'); + let throughputHTML = ''; + + data.stats.forEach(stat => { + throughputHTML += ` +
+ ${stat.iface}: ↓ ${formatBytes(stat.rx_sec)}/s | ↑ ${formatBytes(stat.tx_sec)}/s +
+ `; + }); + + networkThroughput.innerHTML = throughputHTML; + + // Aktualisiere oder erstelle Netzwerk-Chart + updateNetworkChart(data.stats); +}); + +// Netzwerk-Chart initialisieren oder aktualisieren +function updateNetworkChart(stats) { + const ctx = document.getElementById('network-chart').getContext('2d'); + + if (!networkChart) { + // Chart initialisieren, wenn er noch nicht existiert + const labels = []; + const rxData = []; + const txData = []; + + // Fülle anfängliche Daten + for (let i = 0; i < 20; i++) { + labels.push(''); + rxData.push(0); + txData.push(0); + } + + networkChart = new Chart(ctx, { + type: 'line', + data: { + labels: labels, + datasets: [ + { + label: 'Empfangen (Bytes/s)', + data: rxData, + borderColor: '#3f51b5', + backgroundColor: 'rgba(63, 81, 181, 0.1)', + borderWidth: 2, + tension: 0.2, + fill: true + }, + { + label: 'Gesendet (Bytes/s)', + data: txData, + borderColor: '#f44336', + backgroundColor: 'rgba(244, 67, 54, 0.1)', + borderWidth: 2, + tension: 0.2, + fill: true + } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { + beginAtZero: true, + ticks: { + callback: function(value) { + return formatBytes(value, 0); + } + } + } + }, + animation: { + duration: 300 + } + } + }); + } else { + // Chart aktualisieren + let rxTotal = 0; + let txTotal = 0; + + stats.forEach(stat => { + rxTotal += stat.rx_sec || 0; + txTotal += stat.tx_sec || 0; + }); + + // Füge neue Daten hinzu und entferne alte + networkChart.data.labels.push(''); + networkChart.data.labels.shift(); + + networkChart.data.datasets[0].data.push(rxTotal); + networkChart.data.datasets[0].data.shift(); + + networkChart.data.datasets[1].data.push(txTotal); + networkChart.data.datasets[1].data.shift(); + + networkChart.update(); + } +} + +// Hilfsfunktionen +function getColorByPercentage(percent) { + // Farbverlauf von Grün über Gelb nach Rot + if (percent < 70) { + return 'var(--success-color)'; + } else if (percent < 90) { + return 'var(--warning-color)'; + } else { + return 'var(--danger-color)'; + } +} + +function formatBytes(bytes, decimals = 2) { + if (bytes === 0) return '0 B'; + + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i]; +} + +// Initialisierung +document.addEventListener('DOMContentLoaded', () => { + // Zeit aktualisieren + setInterval(() => { + document.getElementById('current-time').innerHTML = `Zeitstempel: ${new Date().toLocaleString('de-DE')}`; + }, 1000); + + // Lade Systemdaten, wenn das System-Panel aktiv ist + if (document.getElementById('system').classList.contains('active')) { + loadSystemInfo(); + } + + // Lade Script für Netzwerk-Chart, falls noch nicht geladen + if (typeof Chart === 'undefined') { + const script = document.createElement('script'); + script.src = 'https://cdn.jsdelivr.net/npm/chart.js'; + script.onload = () => { + console.log('Chart.js geladen'); + }; + document.head.appendChild(script); + } +}); \ No newline at end of file diff --git a/frontend/debug-server/public/views/index.ejs b/frontend/debug-server/public/views/index.ejs new file mode 100644 index 00000000..dc6f21a6 --- /dev/null +++ b/frontend/debug-server/public/views/index.ejs @@ -0,0 +1,227 @@ + + + + + + <%= title %> + + + +

<%= title %>

+

Letzte Aktualisierung: <%= lastCheck %>

+ +
+ +
+

Backend-Konfiguration

+
+ + +
+ + + +
+ +
+

Backend-Status

+
+ Status: <%= backendStatus %> +
+
+ Ping: <%= pingStatus ? 'Erfolgreich' : 'Fehlgeschlagen' %> +
+
+ Host: <%= backendHost %> +
+
+ Port: <%= backendPort %> +
+
+ +
+

Netzwerkschnittstellen

+
    + <% if (interfaces && interfaces.length > 0) { %> + <% interfaces.forEach(function(iface) { %> +
  • + <%= iface.name %>: <%= iface.address %> +
  • + <% }); %> + <% } else { %> +
  • Keine Netzwerkschnittstellen gefunden
  • + <% } %> +
+
+ + + + \ No newline at end of file diff --git a/frontend/debug-server/src/app.js b/frontend/debug-server/src/app.js new file mode 100644 index 00000000..d2611ee0 --- /dev/null +++ b/frontend/debug-server/src/app.js @@ -0,0 +1,235 @@ +const express = require('express'); +const fs = require('fs'); +const path = require('path'); +const axios = require('axios'); +const os = require('os'); +const { execSync } = require('child_process'); + +const app = express(); +const PORT = process.env.PORT || 8081; + +// Konfigurationsdatei +const CONFIG_FILE = path.join(__dirname, '../../../.env.local'); +const DEFAULT_BACKEND_URL = 'http://192.168.0.105:5000'; + +// Middleware +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use(express.static(path.join(__dirname, '../public'))); +app.set('view engine', 'ejs'); +app.set('views', path.join(__dirname, '../public/views')); + +// Hilfsfunktionen +function getBackendUrl() { + try { + if (fs.existsSync(CONFIG_FILE)) { + const content = fs.readFileSync(CONFIG_FILE, 'utf8'); + const match = content.match(/NEXT_PUBLIC_API_URL=(.+)/); + if (match && match[1]) { + return match[1].trim(); + } + } + } catch (error) { + console.error('Fehler beim Lesen der Backend-URL:', error); + } + return DEFAULT_BACKEND_URL; +} + +function setBackendUrl(url) { + try { + let content = ''; + + if (fs.existsSync(CONFIG_FILE)) { + content = fs.readFileSync(CONFIG_FILE, 'utf8'); + // Ersetze die URL, wenn sie bereits existiert + if (content.match(/NEXT_PUBLIC_API_URL=.+/)) { + content = content.replace(/NEXT_PUBLIC_API_URL=.+/, `NEXT_PUBLIC_API_URL=${url}`); + } else { + // Füge die URL hinzu, wenn sie nicht existiert + content += `\nNEXT_PUBLIC_API_URL=${url}`; + } + } else { + // Erstelle eine neue Datei mit der URL + content = `NEXT_PUBLIC_API_URL=${url}`; + } + + fs.writeFileSync(CONFIG_FILE, content, 'utf8'); + return true; + } catch (error) { + console.error('Fehler beim Speichern der Backend-URL:', error); + return false; + } +} + +function getNetworkInterfaces() { + const interfaces = []; + const networkInterfaces = os.networkInterfaces(); + + for (const [name, netInterface] of Object.entries(networkInterfaces)) { + if (name.startsWith('lo') || name.startsWith('docker') || name.startsWith('br-')) { + continue; + } + + for (const iface of netInterface) { + if (iface.family === 'IPv4' || iface.family === 4) { + interfaces.push({ + name: name, + address: iface.address + }); + break; + } + } + } + + return interfaces; +} + +function pingHost(host) { + try { + const platform = process.platform; + const cmd = platform === 'win32' ? + `ping -n 1 -w 1000 ${host}` : + `ping -c 1 -W 1 ${host}`; + + execSync(cmd, { stdio: 'ignore' }); + return true; + } catch (error) { + return false; + } +} + +async function testBackendConnection(url) { + try { + const response = await axios.get(`${url}/api/test`, { timeout: 3000 }); + return response.status === 200; + } catch (error) { + return false; + } +} + +// Routen +app.get('/', async (req, res) => { + const backendUrl = getBackendUrl(); + const interfaces = getNetworkInterfaces(); + + // Analysiere die URL, um Host und Port zu extrahieren + let backendHost = ''; + let backendPort = ''; + + try { + const url = new URL(backendUrl); + backendHost = url.hostname; + backendPort = url.port || (url.protocol === 'https:' ? '443' : '80'); + } catch (error) { + console.error('Ungültige Backend-URL:', error); + } + + // Prüfe Backend-Verbindung + let backendStatus = 'Unbekannt'; + let pingStatus = false; + + if (backendHost) { + pingStatus = pingHost(backendHost); + + if (pingStatus) { + try { + const connectionStatus = await testBackendConnection(backendUrl); + backendStatus = connectionStatus ? 'Verbunden' : 'Keine API-Verbindung'; + } catch (error) { + backendStatus = 'Verbindungsfehler'; + } + } else { + backendStatus = 'Nicht erreichbar'; + } + } + + res.render('index', { + title: 'MYP Frontend Debug', + backendUrl, + backendHost, + backendPort, + backendStatus, + pingStatus, + interfaces, + lastCheck: new Date().toLocaleString('de-DE') + }); +}); + +app.post('/update-backend', async (req, res) => { + const { backendUrl } = req.body; + + if (!backendUrl) { + return res.json({ success: false, message: 'Keine Backend-URL angegeben' }); + } + + // Validiere URL + try { + new URL(backendUrl); + } catch (error) { + return res.json({ success: false, message: 'Ungültige URL' }); + } + + // Speichere die URL + const saved = setBackendUrl(backendUrl); + + if (!saved) { + return res.json({ success: false, message: 'Fehler beim Speichern der URL' }); + } + + // Teste die Verbindung zum Backend + let connectionStatus = false; + + try { + connectionStatus = await testBackendConnection(backendUrl); + } catch (error) { + // Ignoriere Fehler + } + + return res.json({ + success: true, + message: 'Backend-URL erfolgreich aktualisiert', + connection: connectionStatus + }); +}); + +app.post('/test-backend', async (req, res) => { + const { backendUrl } = req.body; + + if (!backendUrl) { + return res.json({ success: false, message: 'Keine Backend-URL angegeben' }); + } + + // Validiere URL + let hostname = ''; + try { + const url = new URL(backendUrl); + hostname = url.hostname; + } catch (error) { + return res.json({ success: false, message: 'Ungültige URL' }); + } + + // Teste Ping + const pingStatus = pingHost(hostname); + + // Teste API-Verbindung + let connectionStatus = false; + + if (pingStatus) { + try { + connectionStatus = await testBackendConnection(backendUrl); + } catch (error) { + // Ignoriere Fehler + } + } + + return res.json({ + success: true, + ping: pingStatus, + connection: connectionStatus + }); +}); + +// Server starten +app.listen(PORT, () => { + console.log(`MYP Frontend Debug Server läuft auf Port ${PORT}`); +}); \ No newline at end of file diff --git a/frontend/debug-server/src/index.js b/frontend/debug-server/src/index.js new file mode 100644 index 00000000..f3bf3f20 --- /dev/null +++ b/frontend/debug-server/src/index.js @@ -0,0 +1,471 @@ +// Frontend Debug-Server für MYP +const express = require('express'); +const path = require('path'); +const http = require('http'); +const si = require('systeminformation'); +const os = require('os'); +const osUtils = require('os-utils'); +const { exec } = require('child_process'); +const fetch = require('node-fetch'); +const socketIo = require('socket.io'); + +// Konfiguration +const app = express(); +const server = http.createServer(app); +const io = socketIo(server); +const PORT = 6666; +const FRONTEND_PORT = 3000; +const FRONTEND_HOST = 'localhost'; +const BACKEND_HOST = 'localhost'; +const BACKEND_PORT = 5000; + +// View Engine einrichten +app.set('view engine', 'ejs'); +app.set('views', path.join(__dirname, '../public/views')); +app.use(express.static(path.join(__dirname, '../public'))); +app.use(express.json()); + +// Hauptseite rendern +app.get('/', async (req, res) => { + const hostname = os.hostname(); + const networkInterfaces = os.networkInterfaces(); + const ipAddresses = {}; + + // IP-Adressen sammeln + Object.keys(networkInterfaces).forEach(interfaceName => { + const interfaceInfo = networkInterfaces[interfaceName]; + const ipv4Addresses = interfaceInfo.filter(info => info.family === 'IPv4'); + if (ipv4Addresses.length > 0) { + ipAddresses[interfaceName] = ipv4Addresses[0].address; + } + }); + + // Rendere die Hauptseite mit Basisdaten + res.render('index', { + hostname: hostname, + ipAddresses: ipAddresses, + timestamp: new Date().toLocaleString('de-DE'), + }); +}); + +// API-Endpunkte + +// Systeminformationen +app.get('/api/system', async (req, res) => { + try { + const [cpu, mem, osInfo, diskLayout, fsSize] = await Promise.all([ + si.cpu(), + si.mem(), + si.osInfo(), + si.diskLayout(), + si.fsSize() + ]); + + const data = { + cpu: { + manufacturer: cpu.manufacturer, + brand: cpu.brand, + speed: cpu.speed, + cores: cpu.cores, + physicalCores: cpu.physicalCores + }, + memory: { + total: formatBytes(mem.total), + free: formatBytes(mem.free), + used: formatBytes(mem.used), + usedPercent: Math.round(mem.used / mem.total * 100) + }, + os: { + platform: osInfo.platform, + distro: osInfo.distro, + release: osInfo.release, + arch: osInfo.arch, + uptime: formatUptime(os.uptime()) + }, + filesystem: fsSize.map(fs => ({ + fs: fs.fs, + type: fs.type, + size: formatBytes(fs.size), + used: formatBytes(fs.used), + available: formatBytes(fs.available), + mount: fs.mount, + usePercent: Math.round(fs.use) + })), + disks: diskLayout.map(disk => ({ + device: disk.device, + type: disk.type, + name: disk.name, + size: formatBytes(disk.size) + })) + }; + + res.json(data); + } catch (error) { + console.error('Fehler beim Abrufen der Systemdaten:', error); + res.status(500).json({ error: 'Fehler beim Abrufen der Systemdaten' }); + } +}); + +// Netzwerkinformationen +app.get('/api/network', async (req, res) => { + try { + const [netInterfaces, netStats] = await Promise.all([ + si.networkInterfaces(), + si.networkStats() + ]); + + const dns = await getDnsServers(); + const gateway = await getDefaultGateway(); + + const data = { + interfaces: netInterfaces.map(iface => ({ + iface: iface.iface, + ip4: iface.ip4, + ip6: iface.ip6, + mac: iface.mac, + internal: iface.internal, + operstate: iface.operstate, + type: iface.type, + speed: iface.speed, + dhcp: iface.dhcp + })), + stats: netStats.map(stat => ({ + iface: stat.iface, + rx_bytes: formatBytes(stat.rx_bytes), + tx_bytes: formatBytes(stat.tx_bytes), + rx_sec: formatBytes(stat.rx_sec), + tx_sec: formatBytes(stat.tx_sec) + })), + dns: dns, + gateway: gateway + }; + + res.json(data); + } catch (error) { + console.error('Fehler beim Abrufen der Netzwerkdaten:', error); + res.status(500).json({ error: 'Fehler beim Abrufen der Netzwerkdaten' }); + } +}); + +// Dienststatus +app.get('/api/services', async (req, res) => { + try { + // Prüfen ob Frontend (Next.js) läuft + const frontendStatus = await checkServiceStatus(FRONTEND_HOST, FRONTEND_PORT); + + // Prüfen ob Backend (Flask) läuft + const backendStatus = await checkServiceStatus(BACKEND_HOST, BACKEND_PORT); + + // Docker-Container Status abrufen + const containers = await getDockerContainers(); + + const data = { + frontend: { + name: 'Next.js Frontend', + status: frontendStatus ? 'online' : 'offline', + port: FRONTEND_PORT, + host: FRONTEND_HOST + }, + backend: { + name: 'Flask Backend', + status: backendStatus ? 'online' : 'offline', + port: BACKEND_PORT, + host: BACKEND_HOST + }, + docker: { + containers: containers + } + }; + + res.json(data); + } catch (error) { + console.error('Fehler beim Abrufen der Dienststatus:', error); + res.status(500).json({ error: 'Fehler beim Abrufen der Dienststatus' }); + } +}); + +// Ping-Endpunkt für Netzwerkdiagnose +app.get('/api/ping/:host', (req, res) => { + const host = req.params.host; + + // Sicherheitscheck für den Hostnamen + if (!isValidHostname(host)) { + return res.status(400).json({ error: 'Ungültiger Hostname oder IP-Adresse' }); + } + + // Ping-Befehl ausführen + exec(`ping -n 4 ${host}`, (error, stdout, stderr) => { + if (error) { + return res.json({ + success: false, + output: stderr || stdout, + error: error.message + }); + } + + res.json({ + success: true, + output: stdout + }); + }); +}); + +// Traceroute-Endpunkt für Netzwerkdiagnose +app.get('/api/traceroute/:host', (req, res) => { + const host = req.params.host; + + // Sicherheitscheck für den Hostnamen + if (!isValidHostname(host)) { + return res.status(400).json({ error: 'Ungültiger Hostname oder IP-Adresse' }); + } + + // Traceroute-Befehl ausführen (Windows: tracert, Unix: traceroute) + const command = process.platform === 'win32' ? 'tracert' : 'traceroute'; + exec(`${command} ${host}`, (error, stdout, stderr) => { + // Traceroute kann einen Nicht-Null-Exit-Code zurückgeben, selbst wenn es teilweise erfolgreich ist + res.json({ + success: true, + output: stdout, + error: stderr + }); + }); +}); + +// DNS-Lookup-Endpunkt für Netzwerkdiagnose +app.get('/api/nslookup/:host', (req, res) => { + const host = req.params.host; + + // Sicherheitscheck für den Hostnamen + if (!isValidHostname(host)) { + return res.status(400).json({ error: 'Ungültiger Hostname oder IP-Adresse' }); + } + + // NSLookup-Befehl ausführen + exec(`nslookup ${host}`, (error, stdout, stderr) => { + if (error) { + return res.json({ + success: false, + output: stderr || stdout, + error: error.message + }); + } + + res.json({ + success: true, + output: stdout + }); + }); +}); + +// Echtzeit-Updates über WebSockets +io.on('connection', (socket) => { + console.log('Neue WebSocket-Verbindung'); + + // CPU- und Arbeitsspeichernutzung im Intervall senden + const systemMonitorInterval = setInterval(async () => { + try { + const [cpu, mem] = await Promise.all([ + si.currentLoad(), + si.mem() + ]); + + socket.emit('system-stats', { + cpu: { + load: Math.round(cpu.currentLoad), + cores: cpu.cpus.map(core => Math.round(core.load)) + }, + memory: { + total: mem.total, + used: mem.used, + free: mem.free, + usedPercent: Math.round(mem.used / mem.total * 100) + } + }); + } catch (error) { + console.error('Fehler beim Senden der Systemstatistiken:', error); + } + }, 2000); + + // Netzwerkstatistiken im Intervall senden + const networkMonitorInterval = setInterval(async () => { + try { + const netStats = await si.networkStats(); + + socket.emit('network-stats', { + stats: netStats.map(stat => ({ + iface: stat.iface, + rx_bytes: stat.rx_bytes, + tx_bytes: stat.tx_bytes, + rx_sec: stat.rx_sec, + tx_sec: stat.tx_sec + })) + }); + } catch (error) { + console.error('Fehler beim Senden der Netzwerkstatistiken:', error); + } + }, 2000); + + // Aufräumen, wenn die Verbindung getrennt wird + socket.on('disconnect', () => { + console.log('WebSocket-Verbindung getrennt'); + clearInterval(systemMonitorInterval); + clearInterval(networkMonitorInterval); + }); +}); + +// Hilfsfunktionen + +// Bytes in lesbare Größen formatieren +function formatBytes(bytes, decimals = 2) { + if (bytes === 0) return '0 B'; + + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i]; +} + +// Uptime in lesbare Zeit formatieren +function formatUptime(seconds) { + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = Math.floor(seconds % 60); + + return `${days} Tage, ${hours} Stunden, ${minutes} Minuten, ${secs} Sekunden`; +} + +// Service-Status überprüfen +async function checkServiceStatus(host, port) { + return new Promise(resolve => { + const socket = new (require('net').Socket)(); + + socket.setTimeout(1000); + + socket.on('connect', () => { + socket.destroy(); + resolve(true); + }); + + socket.on('timeout', () => { + socket.destroy(); + resolve(false); + }); + + socket.on('error', () => { + socket.destroy(); + resolve(false); + }); + + socket.connect(port, host); + }); +} + +// Docker-Container abfragen +async function getDockerContainers() { + return new Promise((resolve) => { + exec('docker ps --format "{{.ID}},{{.Image}},{{.Status}},{{.Ports}},{{.Names}}"', (error, stdout) => { + if (error) { + resolve([]); + return; + } + + const containers = []; + const lines = stdout.trim().split('\n'); + + for (const line of lines) { + if (line) { + const [id, image, status, ports, name] = line.split(','); + containers.push({ id, image, status, ports, name }); + } + } + + resolve(containers); + }); + }); +} + +// DNS-Server abfragen +async function getDnsServers() { + return new Promise((resolve) => { + if (process.platform === 'win32') { + // Windows: DNS-Server über PowerShell abfragen + exec('powershell.exe -Command "Get-DnsClientServerAddress -AddressFamily IPv4 | Select-Object -ExpandProperty ServerAddresses"', (error, stdout) => { + if (error) { + resolve(['DNS-Server konnten nicht ermittelt werden']); + return; + } + + const servers = stdout.trim().split('\r\n').filter(Boolean); + resolve(servers); + }); + } else { + // Unix: DNS-Server aus /etc/resolv.conf lesen + exec('cat /etc/resolv.conf | grep nameserver | cut -d " " -f 2', (error, stdout) => { + if (error) { + resolve(['DNS-Server konnten nicht ermittelt werden']); + return; + } + + const servers = stdout.trim().split('\n').filter(Boolean); + resolve(servers); + }); + } + }); +} + +// Standard-Gateway abfragen +async function getDefaultGateway() { + return new Promise((resolve) => { + if (process.platform === 'win32') { + // Windows: Gateway über PowerShell abfragen + exec('powershell.exe -Command "Get-NetRoute -DestinationPrefix 0.0.0.0/0 | Select-Object -ExpandProperty NextHop"', (error, stdout) => { + if (error) { + resolve('Gateway konnte nicht ermittelt werden'); + return; + } + + resolve(stdout.trim()); + }); + } else { + // Unix: Gateway aus den Routentabellen lesen + exec("ip route | grep default | awk '{print $3}'", (error, stdout) => { + if (error) { + resolve('Gateway konnte nicht ermittelt werden'); + return; + } + + resolve(stdout.trim()); + }); + } + }); +} + +// Validierung des Hostnamens für Sicherheit +function isValidHostname(hostname) { + // Längenprüfung + if (!hostname || hostname.length > 255) { + return false; + } + + // Erlaubte Hostnamen + if (hostname === 'localhost' || hostname === '127.0.0.1') { + return true; + } + + // IPv4-Prüfung + const ipv4Regex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + if (ipv4Regex.test(hostname)) { + return true; + } + + // Hostname-Prüfung + const hostnameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/; + return hostnameRegex.test(hostname); +} + +// Server starten +server.listen(PORT, () => { + console.log(`MYP Frontend Debug-Server läuft auf http://localhost:${PORT}`); +}); \ No newline at end of file diff --git a/frontend/docker-compose.yml b/frontend/docker-compose.yml new file mode 100644 index 00000000..f76b0fdf --- /dev/null +++ b/frontend/docker-compose.yml @@ -0,0 +1,51 @@ +version: '3' + +services: + # Next.js Frontend + frontend: + build: + context: . + dockerfile: Dockerfile + container_name: myp-rp + restart: unless-stopped + environment: + - NODE_ENV=production + - NEXT_PUBLIC_API_URL=https://m040tbaraspi001.de040.corpintra.net/api + networks: + - myp-network + healthcheck: + test: ["CMD", "wget", "--spider", "http://localhost:3000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # Caddy Proxy + caddy: + image: caddy:2.7-alpine + container_name: myp-caddy + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile + - caddy_data:/data + - caddy_config:/config + networks: + - myp-network + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + - CADDY_HOST=53.37.211.254 + - CADDY_DOMAIN=m040tbaraspi001.de040.corpintra.net + cap_add: + - NET_ADMIN + +networks: + myp-network: + driver: bridge + +volumes: + caddy_data: + caddy_config: \ No newline at end of file diff --git a/frontend/docker/build.sh b/frontend/docker/build.sh new file mode 100644 index 00000000..8b04b01b --- /dev/null +++ b/frontend/docker/build.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Define image name +MYP_RP_IMAGE_NAME="myp-rp" + +# Function to build Docker image +build_image() { + local image_name=$1 + local dockerfile=$2 + local platform=$3 + + echo "Building $image_name Docker image for $platform..." + + docker buildx build --platform $platform -t ${image_name}:latest -f $dockerfile --load . + if [ $? -eq 0 ]; then + echo "$image_name Docker image built successfully" + else + echo "Error occurred while building $image_name Docker image" + exit 1 + fi +} + +# Create and use a builder instance (if not already created) +BUILDER_NAME="myp-rp-arm64-builder" +docker buildx create --name $BUILDER_NAME --use || docker buildx use $BUILDER_NAME + +# Build myp-rp image +build_image "$MYP_RP_IMAGE_NAME" "$PWD/Dockerfile" "linux/arm64" + +# Remove the builder instance +docker buildx rm $BUILDER_NAME diff --git a/frontend/docker/caddy/Caddyfile b/frontend/docker/caddy/Caddyfile new file mode 100644 index 00000000..bd371d67 --- /dev/null +++ b/frontend/docker/caddy/Caddyfile @@ -0,0 +1,52 @@ +{ + debug +} + +# Hauptdomain und IP-Adresse für die Anwendung +53.37.211.254, m040tbaraspi001.de040.corpintra.net, m040tbaraspi001, de040.corpintra.net, localhost { + # API Anfragen zum Backend weiterleiten + @api { + path /api/* /health + } + handle @api { + uri strip_prefix /api + reverse_proxy 192.168.0.5:5000 + } + + # Alle anderen Anfragen zum Frontend weiterleiten + handle { + reverse_proxy myp-rp:3000 + } + + tls internal { + on_demand + } + + # Erlaube HTTP -> HTTPS Redirects für OAuth + @oauth path /auth/login/callback* + handle @oauth { + header Cache-Control "no-cache" + reverse_proxy myp-rp:3000 + } + + # Allgemeine Header für Sicherheit und Caching + header { + # Sicherheitsheader + Strict-Transport-Security "max-age=31536000; includeSubDomains" + X-Content-Type-Options "nosniff" + X-Frame-Options "SAMEORIGIN" + Referrer-Policy "strict-origin-when-cross-origin" + + # Cache-Control für statische Assets + @static { + path *.js *.css *.png *.jpg *.svg *.ico *.woff *.woff2 + } + header @static Cache-Control "public, max-age=86400" + + # Keine Caches für dynamische Inhalte + @dynamic { + not path *.js *.css *.png *.jpg *.svg *.ico *.woff *.woff2 + } + header @dynamic Cache-Control "no-store, no-cache, must-revalidate" + } +} \ No newline at end of file diff --git a/frontend/docker/compose.yml b/frontend/docker/compose.yml new file mode 100644 index 00000000..0c79dff6 --- /dev/null +++ b/frontend/docker/compose.yml @@ -0,0 +1,30 @@ +services: + caddy: + image: caddy:2.8 + container_name: caddy + restart: unless-stopped + ports: + - 80:80 + - 443:443 + volumes: + - ./caddy/data:/data + - ./caddy/config:/config + - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro + myp-rp: + image: myp-rp:latest + container_name: myp-rp + environment: + - NEXT_PUBLIC_API_URL=http://192.168.0.105:5000 + - OAUTH_CLIENT_ID=client_id + - OAUTH_CLIENT_SECRET=client_secret + env_file: "/srv/myp-env/github.env" + volumes: + - /srv/MYP-DB:/usr/src/app/db + restart: unless-stopped + # Füge Healthcheck hinzu für besseres Monitoring + healthcheck: + test: ["CMD", "wget", "--spider", "http://localhost:3000"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s diff --git a/frontend/docker/deploy.sh b/frontend/docker/deploy.sh new file mode 100644 index 00000000..09b73d6c --- /dev/null +++ b/frontend/docker/deploy.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Directory containing the Docker images +IMAGE_DIR="docker/images" + +# Load all Docker images from the tar.xz files in the IMAGE_DIR +echo "Loading Docker images from $IMAGE_DIR..." + +for image_file in "$IMAGE_DIR"/*.tar.xz; do + if [ -f "$image_file" ]; then + echo "Loading Docker image from $image_file..." + docker load -i "$image_file" + + # Check if the image loading was successful + if [ $? -ne 0 ]; then + echo "Error occurred while loading Docker image from $image_file" + exit 1 + fi + else + echo "No Docker image tar.xz files found in $IMAGE_DIR." + fi +done + +# Execute docker compose +echo "Running docker compose..." +docker compose -f "docker/compose.yml" up -d + +# Check if the operation was successful +if [ $? -eq 0 ]; then + echo "Docker compose executed successfully" +else + echo "Error occurred while executing docker compose" + exit 1 +fi + +echo "Deployment completed successfully" diff --git a/frontend/docker/images/.gitattributes b/frontend/docker/images/.gitattributes new file mode 100644 index 00000000..8b3e666a --- /dev/null +++ b/frontend/docker/images/.gitattributes @@ -0,0 +1,2 @@ +caddy_2.8.tar.xz filter=lfs diff=lfs merge=lfs -text +myp-rp_latest.tar.xz filter=lfs diff=lfs merge=lfs -text diff --git a/frontend/docker/save.sh b/frontend/docker/save.sh new file mode 100644 index 00000000..2ce2712e --- /dev/null +++ b/frontend/docker/save.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +# Get image name as argument +IMAGE_NAME=$1 +PLATFORM="linux/arm64" + +# Define paths +IMAGE_DIR="docker/images" +IMAGE_FILE="${IMAGE_DIR}/${IMAGE_NAME//[:\/]/_}.tar" +COMPRESSED_FILE="${IMAGE_FILE}.xz" + +# Function to pull the image +pull_image() { + local image=$1 + if [[ $image == arm64v8/* ]]; then + echo "Pulling image $image without platform specification..." + docker pull $image + else + echo "Pulling image $image for platform $PLATFORM..." + docker pull --platform $PLATFORM $image + fi + return $? +} + +# Pull the image if it is not available locally +if ! docker image inspect ${IMAGE_NAME} &>/dev/null; then + if pull_image ${IMAGE_NAME}; then + echo "Image $IMAGE_NAME pulled successfully." + else + echo "Error occurred while pulling $IMAGE_NAME for platform $PLATFORM" + echo "Trying to pull $IMAGE_NAME without platform specification..." + + # Attempt to pull again without platform + if pull_image ${IMAGE_NAME}; then + echo "Image $IMAGE_NAME pulled successfully without platform." + else + echo "Error occurred while pulling $IMAGE_NAME without platform." + echo "Trying to pull arm64v8/${IMAGE_NAME} instead..." + + # Construct new image name + NEW_IMAGE_NAME="arm64v8/${IMAGE_NAME}" + if pull_image ${NEW_IMAGE_NAME}; then + echo "Image $NEW_IMAGE_NAME pulled successfully." + IMAGE_NAME=${NEW_IMAGE_NAME} # Update IMAGE_NAME to use the new one + else + echo "Error occurred while pulling $NEW_IMAGE_NAME" + exit 1 + fi + fi + fi +else + echo "Image $IMAGE_NAME found locally. Skipping pull." +fi + +# Save the Docker image +echo "Saving $IMAGE_NAME Docker image..." +docker save ${IMAGE_NAME} > $IMAGE_FILE + +# Compress the Docker image (overwriting if file exists) +echo "Compressing $IMAGE_FILE..." +xz -z --force $IMAGE_FILE + +if [ $? -eq 0 ]; then + echo "$IMAGE_NAME Docker image saved and compressed successfully as $COMPRESSED_FILE" +else + echo "Error occurred while compressing $IMAGE_NAME Docker image" + exit 1 +fi diff --git a/frontend/docs/Admin-Dashboard.md b/frontend/docs/Admin-Dashboard.md new file mode 100644 index 00000000..e192aa69 --- /dev/null +++ b/frontend/docs/Admin-Dashboard.md @@ -0,0 +1,116 @@ +# **Detaillierte Dokumentation des Admin-Dashboards** + +In diesem Abschnitt werde ich die Funktionen und Nutzung des Admin-Dashboards genauer beschreiben, einschließlich der verschiedenen Module, Diagramme und deren Zweck. + +--- + +## **1. Überblick über das Admin-Dashboard** + +Das Admin-Dashboard ist der zentrale Verwaltungsbereich für Administratoren. Es bietet Funktionen wie die Verwaltung von Druckern, Benutzern und Druckaufträgen sowie detaillierte Statistiken und Analysen. + +### **1.1. Navigation** +Das Dashboard enthält ein Sidebar-Menü mit den folgenden Hauptbereichen: +1. **Dashboard:** Übersicht der wichtigsten Statistiken. +2. **Benutzer:** Verwaltung von Benutzerkonten. +3. **Drucker:** Hinzufügen, Bearbeiten und Verwalten von Druckern. +4. **Druckaufträge:** Einsicht in alle Druckaufträge und deren Status. +5. **Einstellungen:** Konfiguration der Anwendung. +6. **Über MYP:** Informationen über das Projekt und den Entwickler. + +Die Sidebar wird in der Datei `src/app/admin/admin-sidebar.tsx` definiert und dynamisch basierend auf der aktuellen Seite hervorgehoben. + +--- + +## **2. Funktionen des Admin-Dashboards** + +### **2.1. Benutzerverwaltung** +- **Datei:** `src/app/admin/users/page.tsx` +- **Beschreibung:** Ermöglicht das Anzeigen, Bearbeiten und Löschen von Benutzerkonten. +- **Funktionen:** + - Anzeige einer Liste aller registrierten Benutzer. + - Bearbeiten von Benutzerrollen (z. B. „admin“ oder „user“). + - Deaktivieren oder Löschen von Benutzerkonten. + +--- + +### **2.2. Druckerverwaltung** +- **Datei:** `src/app/admin/printers/page.tsx` +- **Beschreibung:** Verwaltung der Drucker, einschließlich Hinzufügen, Bearbeiten und Deaktivieren. +- **Funktionen:** + - Statusanzeige der Drucker (aktiv/inaktiv). + - Hinzufügen neuer Drucker mit Namen und Beschreibung. + - Löschen oder Bearbeiten bestehender Drucker. + +--- + +### **2.3. Druckaufträge** +- **Datei:** `src/app/admin/jobs/page.tsx` +- **Beschreibung:** Übersicht aller Druckaufträge, einschließlich Details wie Startzeit, Dauer und Status. +- **Funktionen:** + - Filtern nach Benutzern, Druckern oder Status (abgeschlossen, abgebrochen). + - Anzeigen von Abbruchgründen und Fehlermeldungen. + - Sortieren nach Zeit oder Benutzer. + +--- + +### **2.4. Einstellungen** +- **Datei:** `src/app/admin/settings/page.tsx` +- **Beschreibung:** Konfigurationsseite für die Anwendung. +- **Funktionen:** + - Ändern von globalen Einstellungen wie Standardzeiten oder Fehlerrichtlinien. + - Download von Daten (z. B. Export der Druckhistorie). + +--- + +## **3. Statistiken und Diagramme** + +Das Admin-Dashboard enthält interaktive Diagramme, die wichtige Statistiken visualisieren. Hier einige der zentralen Diagramme: + +### **3.1. Abbruchgründe** +- **Datei:** `src/app/admin/charts/printer-error-chart.tsx` +- **Beschreibung:** Zeigt die Häufigkeit der Abbruchgründe für Druckaufträge in einem Balkendiagramm. +- **Nutzen:** Identifiziert häufige Probleme wie Materialmangel oder Düsenverstopfungen. + +--- + +### **3.2. Fehlerraten** +- **Datei:** `src/app/admin/charts/printer-error-rate.tsx` +- **Beschreibung:** Zeigt die prozentuale Fehlerrate für jeden Drucker in einem Balkendiagramm. +- **Nutzen:** Ermöglicht die Überwachung und Identifizierung von problematischen Druckern. + +--- + +### **3.3. Druckvolumen** +- **Datei:** `src/app/admin/charts/printer-volume.tsx` +- **Beschreibung:** Zeigt das Druckvolumen für heute, diese Woche und diesen Monat. +- **Nutzen:** Vergleich des Druckeroutputs über verschiedene Zeiträume. + +--- + +### **3.4. Prognostizierte Nutzung** +- **Datei:** `src/app/admin/charts/printer-forecast.tsx` +- **Beschreibung:** Ein Bereichsdiagramm zeigt die erwartete Druckernutzung pro Wochentag. +- **Nutzen:** Hilft bei der Planung von Wartungsarbeiten oder Ressourcenzuweisungen. + +--- + +### **3.5. Druckerauslastung** +- **Datei:** `src/app/admin/charts/printer-utilization.tsx` +- **Beschreibung:** Zeigt die aktuelle Nutzung eines Druckers in Prozent in einem Kreisdiagramm. +- **Nutzen:** Überwacht die Auslastung und identifiziert ungenutzte Ressourcen. + +--- + +## **4. Rollenbasierte Zugriffssteuerung** + +Das Admin-Dashboard ist nur für Benutzer mit der Rolle „admin“ zugänglich. Nicht berechtigte Benutzer werden auf die Startseite umgeleitet. Die Zugriffssteuerung erfolgt durch folgende Logik: +- **Datei:** `src/app/admin/layout.tsx` +- **Funktion:** `validateRequest` prüft die Rolle des aktuellen Benutzers. +- **Umleitung:** Falls die Rolle unzureichend ist, wird der Benutzer automatisch umgeleitet: + ```typescript + if (guard(user, IS_NOT, UserRole.ADMIN)) { + redirect("/"); + } + ``` + +Nächster Schritt: [=> API-Endpunkte und deren Nutzung](./API.md) \ No newline at end of file diff --git a/frontend/docs/Architektur.md b/frontend/docs/Architektur.md new file mode 100644 index 00000000..f64fc96b --- /dev/null +++ b/frontend/docs/Architektur.md @@ -0,0 +1,79 @@ +# **Technische Architektur und Codeaufbau** + +In diesem Abschnitt erläutere ich die Architektur und Struktur des MYP-Projekts sowie die Funktionalitäten der zentralen Komponenten. + +--- + +## **1. Technische Architektur** + +### **1.1. Architekturübersicht** +MYP basiert auf einer modernen Webanwendungsarchitektur: +- **Frontend:** Entwickelt mit React und Next.js. Stellt die Benutzeroberfläche bereit. +- **Backend:** Nutzt Node.js und Drizzle ORM für die Datenbankinteraktion und Geschäftslogik. +- **Datenbank:** SQLite zur Speicherung von Nutzerdaten, Druckaufträgen und Druckerkonfigurationen. +- **Containerisierung:** Docker wird verwendet, um die Anwendung in isolierten Containern bereitzustellen. +- **Webserver:** Caddy dient als Reverse Proxy mit HTTPS-Unterstützung. + +### **1.2. Modulübersicht** +- **Datenfluss:** Die Anwendung ist stark datengetrieben. API-Routen werden genutzt, um Daten zwischen Frontend und Backend auszutauschen. +- **Rollenbasierter Zugriff:** Über ein Berechtigungssystem können Administratoren und Benutzer unterschiedliche Funktionen nutzen. + +--- + +## **2. Codeaufbau** + +### **2.1. Ordnerstruktur** +Die Datei `repomix-output.txt` zeigt eine strukturierte Übersicht des Projekts. Nachfolgend einige wichtige Verzeichnisse: + +| **Verzeichnis** | **Inhalt** | +|--------------------------|---------------------------------------------------------------------------| +| `src/app` | Next.js-Seiten und Komponenten für Benutzer und Admins. | +| `src/components` | Wiederverwendbare UI-Komponenten wie Karten, Diagramme, Buttons etc. | +| `src/server` | Backend-Logik, Authentifizierung und Datenbankinteraktionen. | +| `src/utils` | Hilfsfunktionen für Analysen, Validierungen und Datenbankzugriffe. | +| `drizzle` | Datenbank-Migrationsdateien und Metadaten. | +| `docker` | Docker-Konfigurations- und Bereitstellungsskripte. | + +--- + +### **2.2. Hauptdateien** +#### **Frontend** +- **`src/app/page.tsx`:** Startseite der Anwendung. +- **`src/app/admin/`:** Admin-spezifische Seiten, z. B. Druckerverwaltung oder Fehlerstatistiken. +- **`src/components/ui/`:** UI-Komponenten wie Dialoge, Formulare und Tabellen. + +#### **Backend** +- **`src/server/auth/`:** Authentifizierung und Benutzerrollenmanagement. +- **`src/server/actions/`:** Funktionen zur Interaktion mit Druckaufträgen und Druckern. +- **`src/utils/`:** Analyse und Verarbeitung von Druckdaten (z. B. Fehlerquoten und Auslastung). + +#### **Datenbank** +- **`drizzle/0000_overjoyed_strong_guy.sql`:** SQLite-Datenbankschema mit Tabellen für Drucker, Benutzer und Druckaufträge. +- **`drizzle.meta/`:** Metadaten zur Datenbankmigration. + +--- + +### **2.3. Datenbankschema** +Das Schema enthält vier Haupttabellen: +1. **`user`:** Speichert Benutzerinformationen, einschließlich Rollen und E-Mail-Adressen. +2. **`printer`:** Beschreibt die Drucker, ihren Status und ihre Eigenschaften. +3. **`printJob`:** Zeichnet Druckaufträge auf, einschließlich Startzeit, Dauer und Abbruchgrund. +4. **`session`:** Verwaltert Benutzer-Sitzungen und Ablaufzeiten. + +--- + +## **3. Wichtige Funktionen** + +### **3.1. Authentifizierung** +Das System nutzt OAuth zur Anmeldung. Benutzerrollen werden in der Tabelle `user` gespeichert und im Backend überprüft. + +### **3.2. Statistiken** +- **Fehlerrate:** Berechnet die Häufigkeit von Abbrüchen für jeden Drucker. +- **Auslastung:** Prozentuale Nutzung der Drucker, basierend auf geplanten und abgeschlossenen Druckaufträgen. +- **Prognosen:** Verwenden historische Daten, um zukünftige Drucknutzungen vorherzusagen. + +### **3.3. API-Endpunkte** +- **`src/app/api/printers/`:** Zugriff auf Druckerkonfigurationsdaten. +- **`src/app/api/job/[jobId]/`:** Verwaltung einzelner Druckaufträge. + +Nächster Schritt: [=> Datenbank und Analytik-Funktionen](./Datenbank.md) \ No newline at end of file diff --git a/frontend/docs/Bereitstellungsdetails .md b/frontend/docs/Bereitstellungsdetails .md new file mode 100644 index 00000000..78a5b5a3 --- /dev/null +++ b/frontend/docs/Bereitstellungsdetails .md @@ -0,0 +1,150 @@ +# **Bereitstellungsdetails und Best Practices** + +In diesem Abschnitt erläutere ich, wie das MYP-Projekt auf einem Server bereitgestellt wird, sowie empfohlene Praktiken zur Verwaltung und Optimierung des Systems. + +--- + +## **1. Bereitstellungsschritte** + +### **1.1. Voraussetzungen** +- **Server:** Raspberry Pi mit installiertem Raspbian Lite. +- **Docker:** Docker und Docker Compose müssen vorab installiert sein. +- **Netzwerk:** Der Server muss über eine statische IP-Adresse oder einen DNS-Namen erreichbar sein. + +### **1.2. Vorbereitung** +#### **1.2.1. Docker-Images erstellen und speichern** +Führen Sie die folgenden Schritte auf dem Entwicklungssystem aus: +1. **Images erstellen:** + ```bash + bash docker/build.sh + ``` +2. **Images exportieren und komprimieren:** + ```bash + bash docker/save.sh + ``` + Dies speichert die Docker-Images im Verzeichnis `docker/images/`. + +#### **1.2.2. Übertragung auf den Server** +Kopieren Sie die erzeugten `.tar.xz`-Dateien auf den Raspberry Pi: +```bash +scp docker/images/*.tar.xz @:/path/to/destination/ +``` + +--- + +### **1.3. Images auf dem Server laden** +Loggen Sie sich auf dem Server ein und laden Sie die Docker-Images: +```bash +docker load -i /path/to/destination/.tar.xz +``` + +--- + +### **1.4. Starten der Anwendung** +Führen Sie das Bereitstellungsskript aus: +```bash +bash docker/deploy.sh +``` +Dieses Skript: +- Startet die Docker-Container mithilfe von `docker compose`. +- Verbindet den Reverse Proxy (Caddy) mit der Anwendung. + +Die Anwendung sollte unter `http://` oder der konfigurierten Domain erreichbar sein. + +--- + +## **2. Best Practices** + +### **2.1. Sicherheit** +1. **Umgebungsvariablen schützen:** + - Stellen Sie sicher, dass die Datei `.env` nicht versehentlich in ein öffentliches Repository hochgeladen wird. + - Verwenden Sie geeignete Zugriffsrechte: + ```bash + chmod 600 .env + ``` +2. **HTTPS aktivieren:** + - Der Caddy-Webserver unterstützt automatisch HTTPS. Stellen Sie sicher, dass eine gültige Domain konfiguriert ist. + +3. **Zugriffsrechte beschränken:** + - Verwenden Sie Benutzerrollen („admin“, „guest“), um den Zugriff auf kritische Funktionen zu steuern. + +--- + +### **2.2. Performance** +1. **Docker-Container optimieren:** + - Reduzieren Sie die Größe der Docker-Images, indem Sie unnötige Dateien in `.dockerignore` ausschließen. + +2. **Datenbankwartung:** + - Führen Sie regelmäßige Backups der SQLite-Datenbank durch: + ```bash + cp db/sqlite.db /path/to/backup/location/ + ``` + - Optimieren Sie die Datenbank regelmäßig: + ```sql + VACUUM; + ``` + +3. **Skalierung:** + - Bei hoher Last kann die Anwendung mit Kubernetes oder einer Cloud-Lösung (z. B. AWS oder Azure) skaliert werden. + +--- + +### **2.3. Fehlerbehebung** +1. **Logs überprüfen:** + - Docker-Logs können wichtige Debug-Informationen liefern: + ```bash + docker logs + ``` + +2. **Health Checks:** + - Integrieren Sie Health Checks in die Docker Compose-Datei, um sicherzustellen, dass die Dienste korrekt laufen. + +3. **Fehlerhafte Drucker deaktivieren:** + - Deaktivieren Sie Drucker mit einer hohen Fehlerrate über das Admin-Dashboard, um die Benutzererfahrung zu verbessern. + +--- + +### **2.4. Updates** +1. **Neue Funktionen hinzufügen:** + - Aktualisieren Sie die Anwendung und erstellen Sie neue Docker-Images: + ```bash + git pull origin main + bash docker/build.sh + ``` + - Stellen Sie die aktualisierten Images bereit: + ```bash + bash docker/deploy.sh + ``` + +2. **Datenbankmigrationen:** + - Führen Sie neue Migrationsskripte mit folgendem Befehl aus: + ```bash + pnpm run db:migrate + ``` + +--- + +## **3. Backup und Wiederherstellung** + +### **3.1. Backups erstellen** +Sichern Sie wichtige Dateien und Datenbanken regelmäßig: +- **SQLite-Datenbank:** + ```bash + cp db/sqlite.db /backup/location/sqlite-$(date +%F).db + ``` +- **Docker-Images:** + ```bash + docker save myp-rp:latest | gzip > /backup/location/myp-rp-$(date +%F).tar.gz + ``` + +### **3.2. Wiederherstellung** +- **Datenbank wiederherstellen:** + ```bash + cp /backup/location/sqlite-.db db/sqlite.db + ``` +- **Docker-Images importieren:** + ```bash + docker load < /backup/location/myp-rp-.tar.gz + ``` + +Nächster Schritt: [=> Admin-Dashboard](./Admin-Dashboard.md) \ No newline at end of file diff --git a/frontend/docs/Datenbank.md b/frontend/docs/Datenbank.md new file mode 100644 index 00000000..253a16ea --- /dev/null +++ b/frontend/docs/Datenbank.md @@ -0,0 +1,153 @@ +# **Datenbank und Analytik-Funktionen** + +Dieser Abschnitt konzentriert sich auf die Struktur der Datenbank sowie die Analyse- und Prognosefunktionen, die im Projekt verwendet werden. + +--- + +## **1. Datenbankstruktur** + +Das Datenbankschema wurde mit **Drizzle ORM** definiert und basiert auf SQLite. Die wichtigsten Tabellen und ihre Zwecke sind: + +### **1.1. Tabellenübersicht** + +#### **`user`** +- Speichert Benutzerinformationen. +- Enthält Rollen wie „admin“ oder „guest“ zur Verwaltung von Berechtigungen. + +| **Feld** | **Typ** | **Beschreibung** | +|-------------------|------------|-------------------------------------------| +| `id` | `text` | Eindeutige ID des Benutzers. | +| `github_id` | `integer` | ID des Benutzers aus dem OAuth-Dienst. | +| `name` | `text` | Benutzername. | +| `displayName` | `text` | Angezeigter Name. | +| `email` | `text` | E-Mail-Adresse. | +| `role` | `text` | Benutzerrolle, Standardwert: „guest“. | + +--- + +#### **`printer`** +- Beschreibt verfügbare Drucker und deren Status. + +| **Feld** | **Typ** | **Beschreibung** | +|-------------------|------------|-------------------------------------------| +| `id` | `text` | Eindeutige Drucker-ID. | +| `name` | `text` | Name des Druckers. | +| `description` | `text` | Beschreibung oder Spezifikationen. | +| `status` | `integer` | Betriebsstatus (0 = inaktiv, 1 = aktiv). | + +--- + +#### **`printJob`** +- Speichert Informationen zu Druckaufträgen. + +| **Feld** | **Typ** | **Beschreibung** | +|-----------------------|---------------|-------------------------------------------------------| +| `id` | `text` | Eindeutige Auftrags-ID. | +| `printerId` | `text` | Verweis auf die ID des Druckers. | +| `userId` | `text` | Verweis auf die ID des Benutzers. | +| `startAt` | `integer` | Startzeit des Druckauftrags (Unix-Timestamp). | +| `durationInMinutes` | `integer` | Dauer des Druckauftrags in Minuten. | +| `comments` | `text` | Zusätzliche Kommentare. | +| `aborted` | `integer` | 1 = Abgebrochen, 0 = Erfolgreich abgeschlossen. | +| `abortReason` | `text` | Grund für den Abbruch (falls zutreffend). | + +--- + +#### **`session`** +- Verwaltert Benutzer-Sitzungen und Ablaufzeiten. + +| **Feld** | **Typ** | **Beschreibung** | +|-------------------|------------|-------------------------------------------| +| `id` | `text` | Eindeutige Sitzungs-ID. | +| `user_id` | `text` | Verweis auf die ID des Benutzers. | +| `expires_at` | `integer` | Zeitpunkt, wann die Sitzung abläuft. | + +--- + +### **1.2. Relationen** +- `printer` → `printJob`: Druckaufträge sind an spezifische Drucker gebunden. +- `user` → `printJob`: Druckaufträge werden Benutzern zugewiesen. +- `user` → `session`: Sitzungen verknüpfen Benutzer mit Login-Details. + +--- + +## **2. Analytik-Funktionen** + +Das Projekt bietet verschiedene Analytik- und Prognosetools, um die Druckernutzung und Fehler zu überwachen. + +### **2.1. Fehlerratenanalyse** +- Funktion: `calculatePrinterErrorRate` (in `src/utils/analytics/error-rate.ts`). +- Berechnet die prozentuale Fehlerrate für jeden Drucker basierend auf abgebrochenen Aufträgen. + +Beispielausgabe: +```json +[ + { "name": "Drucker 1", "errorRate": 5.2 }, + { "name": "Drucker 2", "errorRate": 3.7 } +] +``` + +--- + +### **2.2. Abbruchgründe** +- Funktion: `calculateAbortReasonsCount` (in `src/utils/analytics/errors.ts`). +- Zählt die Häufigkeit der Abbruchgründe aus der Tabelle `printJob`. + +Beispielausgabe: +```json +[ + { "abortReason": "Materialmangel", "count": 10 }, + { "abortReason": "Düsenverstopfung", "count": 7 } +] +``` + +--- + +### **2.3. Nutzung und Prognosen** +#### Nutzung: +- Funktion: `calculatePrinterUtilization` (in `src/utils/analytics/utilization.ts`). +- Berechnet die Nutzung der Drucker in Prozent. + +Beispielausgabe: +```json +{ "printerId": "1", "utilizationPercentage": 85 } +``` + +#### Prognosen: +- Funktion: `forecastPrinterUsage` (in `src/utils/analytics/forecast.ts`). +- Nutzt historische Daten, um die erwartete Druckernutzung für kommende Tage/Wochen zu schätzen. + +Beispielausgabe: +```json +[ + { "day": 1, "usageMinutes": 300 }, + { "day": 2, "usageMinutes": 200 } +] +``` + +--- + +### **2.4. Druckvolumen** +- Funktion: `calculatePrintVolumes` (in `src/utils/analytics/volume.ts`). +- Vergleicht die Anzahl der abgeschlossenen Druckaufträge für heute, diese Woche und diesen Monat. + +Beispielausgabe: +```json +{ + "today": 15, + "thisWeek": 90, + "thisMonth": 300 +} +``` + +--- + +## **3. Datenbankinitialisierung** +Die Datenbank wird über Skripte in der `package.json` initialisiert: +```bash +pnpm run db:clean # Datenbank und Migrationsordner löschen +pnpm run db:generate # Neues Schema generieren +pnpm run db:migrate # Migrationsskripte ausführen +``` + +Nächster Schritt: [=> Bereitstellungsdetails und Best Practices](./Bereitstellungsdetails.md) \ No newline at end of file diff --git a/frontend/docs/Installation.md b/frontend/docs/Installation.md new file mode 100644 index 00000000..c1d866da --- /dev/null +++ b/frontend/docs/Installation.md @@ -0,0 +1,93 @@ +# **Installation und Einrichtung** + +In diesem Abschnitt wird beschrieben, wie die MYP-Anwendung installiert und eingerichtet wird. Diese Schritte umfassen die Vorbereitung der Umgebung, das Konfigurieren der notwendigen Dienste und die Bereitstellung des Projekts. + +--- + +## **Voraussetzungen** +### **Hardware und Software** +- **Raspberry Pi:** Die Anwendung ist für den Einsatz auf einem Raspberry Pi optimiert, auf dem Raspbian Lite installiert sein sollte. +- **Docker:** Docker und Docker Compose müssen installiert sein. +- **Netzwerkzugriff:** Der Raspberry Pi muss im Netzwerk erreichbar sein. + +### **Abhängigkeiten** +- Node.js (mindestens Version 20) +- PNPM (Paketmanager) +- SQLite (für lokale Datenbankverwaltung) + +--- + +## **Schritte zur Einrichtung** + +### **1. Repository klonen** +Klonen Sie das Repository auf Ihr System: +```bash +git clone +cd +``` + +### **2. Konfiguration der Umgebungsvariablen** +Passen Sie die Datei `.env.example` an und benennen Sie sie in `.env` um: +```bash +cp .env.example .env +``` +Erforderliche Variablen: +- `OAUTH_CLIENT_ID`: Client-ID für die OAuth-Authentifizierung +- `OAUTH_CLIENT_SECRET`: Geheimnis für die OAuth-Authentifizierung + +### **3. Docker-Container erstellen** +Führen Sie das Skript `build.sh` aus, um Docker-Images zu erstellen: +```bash +bash docker/build.sh +``` +Dies erstellt die notwendigen Docker-Images, einschließlich der Anwendung und eines Caddy-Webservers. + +### **4. Docker-Images speichern** +Speichern Sie die Images in komprimierter Form, um sie auf anderen Geräten bereitzustellen: +```bash +bash docker/save.sh +``` + +### **5. Bereitstellung** +Kopieren Sie die Docker-Images auf den Zielserver (z. B. Raspberry Pi) und führen Sie `deploy.sh` aus: +```bash +scp docker/images/*.tar.xz :/path/to/deployment/ +bash docker/deploy.sh +``` +Das Skript führt die Docker Compose-Konfiguration aus und startet die Anwendung. + +### **(Optional: 6. Admin-User anlegen)** + +Um einen Admin-User anzulegen, muss zuerst das Container-Image gestartet werden. Anschließend meldet man sich mittels +der GitHub-Authentifizierung bei der Anwendung an. + +Der nun in der Datenbank angelegte User hat die Rolle `guest`. Über das CLI muss man nun in die SQLite-Datenbank (die Datenbank sollte außerhalb des Container-Images liegen) wechseln und +den User updaten. + + +#### SQL-Befehl, um den User zu updaten: +```bash +sqlite3 db.sqlite3 +UPDATE users SET role = 'admin' WHERE id = ; +``` + +--- + +## **Start der Anwendung** +Sobald die Docker-Container laufen, ist die Anwendung unter der angegebenen Domain oder IP-Adresse erreichbar. Standardmäßig verwendet der Caddy-Webserver Port 80 (HTTP) und 443 (HTTPS). + +--- + +## **Optional: Entwicklungsmodus** +Für lokale Tests können Sie die Anwendung ohne Docker starten: +1. Installieren Sie Abhängigkeiten: + ```bash + pnpm install + ``` +2. Starten Sie den Entwicklungsserver: + ```bash + pnpm dev + ``` + Die Anwendung ist dann unter `http://localhost:3000` verfügbar. + +Nächster Schritt: [=> Nutzung](./Nutzung.md) \ No newline at end of file diff --git a/frontend/docs/Nutzung.md b/frontend/docs/Nutzung.md new file mode 100644 index 00000000..2080933b --- /dev/null +++ b/frontend/docs/Nutzung.md @@ -0,0 +1,75 @@ +# **Features und Nutzung der Anwendung** + +In diesem Abschnitt beschreibe ich die Hauptfunktionen von MYP (Manage Your Printer) und gebe Anweisungen zur Nutzung der verschiedenen Module. + +--- + +## **1. Hauptfunktionen** + +### **1.1. Druckerreservierung** +- Nutzer können Drucker für einen definierten Zeitraum reservieren. +- Konflikte bei Reservierungen werden durch ein Echtzeit-Überprüfungssystem verhindert. + +### **1.2. Fehler- und Auslastungsanalyse** +- Darstellung von Druckfehlern nach Kategorien und Häufigkeiten. +- Übersicht der aktuellen und historischen Druckernutzung. +- Diagramme zur Fehlerrate, Nutzung und Druckvolumen. + +### **1.3. Admin-Dashboard** +- Verwaltung von Druckern, Nutzern und Druckaufträgen. +- Überblick über alle Abbruchgründe und Druckfehler. +- Zugriff auf erweiterte Statistiken und Prognosen. + +--- + +## **2. Nutzung der Anwendung** + +### **2.1. Login und Authentifizierung** +- Die Anwendung unterstützt OAuth-basierte Authentifizierung. +- Nutzer müssen sich mit einem gültigen Konto anmelden, um Zugriff auf die Funktionen zu erhalten. + +### **2.2. Dashboard** +- Nach dem Login gelangen die Nutzer auf das Dashboard, das einen Überblick über die aktuelle Druckernutzung bietet. +- Administratoren haben Zugriff auf zusätzliche Menüpunkte, wie z. B. Benutzerverwaltung. + +--- + +## **3. Admin-Funktionen** + +### **3.1. Druckerverwaltung** +- Administratoren können Drucker hinzufügen, bearbeiten oder löschen. +- Status eines Druckers (z. B. „in Betrieb“, „außer Betrieb“) kann angepasst werden. + +### **3.2. Nutzerverwaltung** +- Verwalten von Benutzerkonten, einschließlich Rollen (z. B. „Admin“ oder „User“). +- Benutzer können aktiviert oder deaktiviert werden. + +### **3.3. Statistiken und Berichte** +- Diagramme wie: + - **Abbruchgründe:** Zeigt häufige Fehlerursachen. + - **Fehlerrate:** Prozentuale Fehlerquote der Drucker. + - **Nutzung:** Prognosen für die Druckernutzung pro Wochentag. + +--- + +## **4. Diagramme und Visualisierungen** + +### **4.1. Abbruchgründe** +- Ein Säulendiagramm zeigt die Häufigkeiten der Fehlerursachen. +- Nutzt Echtzeit-Daten aus der Druckhistorie. + +### **4.2. Prognostizierte Nutzung** +- Ein Liniendiagramm zeigt die erwartete Druckernutzung pro Tag. +- Hilft bei der Planung von Wartungszeiten. + +### **4.3. Druckvolumen** +- Balkendiagramme vergleichen Druckaufträge heute, diese Woche und diesen Monat. + +--- + +## **5. Interaktive Komponenten** +- **Benachrichtigungen:** Informieren über Druckaufträge, Fehler oder Systemereignisse. +- **Filter und Suchfunktionen:** Erleichtern das Auffinden von Druckern oder Druckaufträgen. +- **Rollenbasierter Zugriff:** Funktionen sind je nach Benutzerrolle eingeschränkt. + +Nächster Schritt: [=> Technische Architektur und Codeaufbau](./Architektur.md) \ No newline at end of file diff --git a/frontend/docs/README.md b/frontend/docs/README.md new file mode 100644 index 00000000..741f73b2 --- /dev/null +++ b/frontend/docs/README.md @@ -0,0 +1,37 @@ +# **Einleitung** + +> Information: Die Dokumenation wurde mit generativer AI erstellt und kann fehlerhaft sein. Im Zweifel bitte die Quellcode-Dateien anschauen oder die Entwickler kontaktieren. + +## **Projektbeschreibung** +MYP (Manage Your Printer) ist eine Webanwendung zur Verwaltung und Reservierung von 3D-Druckern. Das Projekt wurde als Abschlussarbeit im Rahmen der Fachinformatiker-Ausbildung mit Schwerpunkt Daten- und Prozessanalyse entwickelt und dient als Plattform zur einfachen Koordination und Überwachung von Druckressourcen. Es wurde speziell für die Technische Berufsausbildung des Mercedes-Benz Werkes in Berlin-Marienfelde erstellt. + +--- + +## **Hauptmerkmale** +- **Druckerreservierungen:** Nutzer können 3D-Drucker in definierten Zeitfenstern reservieren. +- **Fehleranalyse:** Statistiken über Druckfehler und Abbruchgründe werden visuell dargestellt. +- **Druckauslastung:** Echtzeit-Daten über die Nutzung der Drucker. +- **Admin-Dashboard:** Übersichtliche Verwaltung und Konfiguration von Druckern, Benutzern und Druckaufträgen. +- **Datenbankintegration:** Alle Daten werden in einer SQLite-Datenbank gespeichert und verwaltet. + +--- + +## **Technologien** +- **Frontend:** React, Next.js, TailwindCSS +- **Backend:** Node.js, Drizzle ORM +- **Datenbank:** SQLite +- **Deployment:** Docker und Raspberry Pi +- **Zusätzliche Bibliotheken:** recharts für Diagramme, Faker.js für Testdaten, sowie diverse Radix-UI-Komponenten. + +--- + +## **Dateistruktur** +Die Repository-Dateien sind in logische Abschnitte unterteilt: +1. **Docker-Konfigurationen** (`docker/`) - Skripte und Konfigurationsdateien für die Bereitstellung. +2. **Frontend-Komponenten** (`src/app/`) - Weboberfläche und deren Funktionalitäten. +3. **Backend-Funktionen** (`src/server/`) - Datenbankinteraktionen und Authentifizierungslogik. +4. **Utils und Helferfunktionen** (`src/utils/`) - Wiederverwendbare Dienste und Hilfsmethoden. +5. **Datenbank-Skripte** (`drizzle/`) - Datenbankschemas und Migrationsdateien. + + +Nächster Schritt: [=> Installation](./Installation.md) diff --git a/frontend/drizzle.config.ts b/frontend/drizzle.config.ts new file mode 100644 index 00000000..965ecba7 --- /dev/null +++ b/frontend/drizzle.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "drizzle-kit"; + +//@ts-ignore - better-sqlite driver throws an error even though its an valid value +export default defineConfig({ + dialect: "sqlite", + schema: "./src/server/db/schema.ts", + out: "./drizzle", + driver: "libsql", + dbCredentials: { + url: "file:./db/sqlite.db", + }, +}); diff --git a/frontend/drizzle/0000_overjoyed_strong_guy.sql b/frontend/drizzle/0000_overjoyed_strong_guy.sql new file mode 100644 index 00000000..13501e10 --- /dev/null +++ b/frontend/drizzle/0000_overjoyed_strong_guy.sql @@ -0,0 +1,35 @@ +CREATE TABLE `printJob` ( + `id` text PRIMARY KEY NOT NULL, + `printerId` text NOT NULL, + `userId` text NOT NULL, + `startAt` integer NOT NULL, + `durationInMinutes` integer NOT NULL, + `comments` text, + `aborted` integer DEFAULT false NOT NULL, + `abortReason` text, + FOREIGN KEY (`printerId`) REFERENCES `printer`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `printer` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `description` text NOT NULL, + `status` integer DEFAULT 0 NOT NULL +); +--> statement-breakpoint +CREATE TABLE `session` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `expires_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `user` ( + `id` text PRIMARY KEY NOT NULL, + `github_id` integer NOT NULL, + `name` text, + `displayName` text, + `email` text NOT NULL, + `role` text DEFAULT 'guest' +); diff --git a/frontend/drizzle/meta/0000_snapshot.json b/frontend/drizzle/meta/0000_snapshot.json new file mode 100644 index 00000000..6a12d0d9 --- /dev/null +++ b/frontend/drizzle/meta/0000_snapshot.json @@ -0,0 +1,241 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "791dc197-5254-4432-bd9f-1368d1a5aa6a", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "printJob": { + "name": "printJob", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "printerId": { + "name": "printerId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "startAt": { + "name": "startAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "durationInMinutes": { + "name": "durationInMinutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "comments": { + "name": "comments", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aborted": { + "name": "aborted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "abortReason": { + "name": "abortReason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "printJob_printerId_printer_id_fk": { + "name": "printJob_printerId_printer_id_fk", + "tableFrom": "printJob", + "tableTo": "printer", + "columnsFrom": [ + "printerId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "printJob_userId_user_id_fk": { + "name": "printJob_userId_user_id_fk", + "tableFrom": "printJob", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "printer": { + "name": "printer", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "github_id": { + "name": "github_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "displayName": { + "name": "displayName", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'guest'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/frontend/drizzle/meta/_journal.json b/frontend/drizzle/meta/_journal.json new file mode 100644 index 00000000..73e233ca --- /dev/null +++ b/frontend/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "6", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1715416514336, + "tag": "0000_overjoyed_strong_guy", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/frontend/https-setup.sh b/frontend/https-setup.sh new file mode 100644 index 00000000..4e4065c2 --- /dev/null +++ b/frontend/https-setup.sh @@ -0,0 +1,376 @@ +#!/bin/bash + +# HTTPS-Setup-Skript für das MYP-Projekt +# Konfiguriert einen Raspberry Pi mit einer dual-network-Konfiguration +# - LAN (eth0): Firmennetzwerk mit Zugang zu Internet und unter https://m040tbaraspi001.de040.corpintra.net/ erreichbar +# - WLAN (wlan0): Verbindung zum Offline-Netzwerk, wo der Backend-Host erreichbar ist + +# Farbcodes für Ausgabe +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# Funktion zur Ausgabe mit Zeitstempel +log() { + echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" +} + +error_log() { + echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] FEHLER:${NC} $1" >&2 +} + +success_log() { + echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] ERFOLG:${NC} $1" +} + +header() { + echo "" + echo -e "${CYAN}===== $1 =====${NC}" + echo "" +} + +# Prüfen, ob das Skript als Root ausgeführt wird +if [ "$EUID" -ne 0 ]; then + error_log "Dieses Skript muss als Root ausgeführt werden." + error_log "Bitte führen Sie es mit 'sudo' aus." + exit 1 +fi + +# Konfigurationswerte +FIRMENNETZWERK_HOSTNAME="m040tbaraspi001.de040.corpintra.net" +BACKEND_HOST="192.168.0.105" # Backend-IP im Offline-Netzwerk +BACKEND_PORT="5000" # Backend-Port +OFFLINE_NETWORK_SSID="MYP-Net" +OFFLINE_NETWORK_PASSWORD="myp-password" +CADDY_VERSION="2.7.6" + +header "MYP-Netzwerk und HTTPS-Setup" +log "Dieses Skript konfiguriert Ihren Raspberry Pi für:" +log "1. Firmennetzwerk über LAN (eth0) mit Internet-Zugang" +log "2. Offline-Netzwerk über WLAN (wlan0) für Backend-Kommunikation" +log "3. HTTPS mit selbstsigniertem Zertifikat für ${FIRMENNETZWERK_HOSTNAME}" + +# Netzwerkkonfiguration +setup_network() { + header "Netzwerkkonfiguration" + + # Sichern der aktuellen Netzwerkkonfiguration + log "Sichere aktuelle Netzwerkkonfiguration..." + if [ -f /etc/dhcpcd.conf ]; then + cp /etc/dhcpcd.conf /etc/dhcpcd.conf.bak + success_log "Aktuelle Netzwerkkonfiguration gesichert in /etc/dhcpcd.conf.bak" + fi + + # Konfigurieren von dhcpcd.conf für statische Routing + log "Konfiguriere Routing für duale Netzwerke..." + cat > /etc/dhcpcd.conf << EOL +# MYP Dual-Network Configuration +# eth0: Firmennetzwerk mit Internet +# wlan0: Offline-Netzwerk für Backend + +# Allow dhcpcd to manage all interfaces +allowinterfaces eth0 wlan0 + +# eth0 configuration (Firmennetzwerk) +interface eth0 +# DHCP for eth0, all default routes go through eth0 +metric 100 + +# wlan0 configuration (Offline Network) +interface wlan0 +# Static IP for wlan0 +metric 200 +# Add specific route to backend via wlan0 +EOL + + # Konfigurieren von wpa_supplicant für WLAN-Verbindung + log "Konfiguriere WLAN-Verbindung für Offline-Netzwerk..." + cat > /etc/wpa_supplicant/wpa_supplicant.conf << EOL +ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev +update_config=1 +country=DE + +network={ + ssid="${OFFLINE_NETWORK_SSID}" + psk="${OFFLINE_NETWORK_PASSWORD}" + priority=1 +} +EOL + + chmod 600 /etc/wpa_supplicant/wpa_supplicant.conf + + # Routing-Tabelle konfigurieren + log "Konfiguriere spezifisches Routing zum Backend..." + if ! grep -q "${BACKEND_HOST}" /etc/iproute2/rt_tables; then + echo "200 backend" >> /etc/iproute2/rt_tables + fi + + # Routing-Regeln in /etc/network/if-up.d/ hinzufügen + cat > /etc/network/if-up.d/route-backend << EOL +#!/bin/bash +# Routing-Regeln für Backend-Host über WLAN + +# Wenn wlan0 hochgefahren wird +if [ "\$IFACE" = "wlan0" ]; then + # Spezifische Route zum Backend über wlan0 + /sbin/ip route add ${BACKEND_HOST}/32 dev wlan0 +fi +EOL + + chmod +x /etc/network/if-up.d/route-backend + + success_log "Netzwerkkonfiguration abgeschlossen" +} + +# Installation von Caddy als Reverse-Proxy +install_caddy() { + header "Installation von Caddy als Reverse-Proxy" + + log "Überprüfe, ob Caddy bereits installiert ist..." + if command -v caddy &> /dev/null; then + success_log "Caddy ist bereits installiert" + else + log "Installiere Caddy ${CADDY_VERSION}..." + + # Download und Installation von Caddy + wget -O /tmp/caddy.tar.gz "https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_linux_armv7.tar.gz" + + if [ $? -ne 0 ]; then + error_log "Fehler beim Herunterladen von Caddy" + return 1 + fi + + tar -xzf /tmp/caddy.tar.gz -C /tmp + mv /tmp/caddy /usr/local/bin/ + chmod +x /usr/local/bin/caddy + + # Benutzer und Gruppe für Caddy erstellen + if ! id -u caddy &>/dev/null; then + useradd --system --home /var/lib/caddy --shell /usr/sbin/nologin caddy + fi + + # Verzeichnisse für Caddy erstellen + mkdir -p /etc/caddy /var/lib/caddy /var/log/caddy + chown -R caddy:caddy /var/lib/caddy /var/log/caddy + + # Systemd-Service für Caddy einrichten + cat > /etc/systemd/system/caddy.service << EOL +[Unit] +Description=Caddy Web Server +Documentation=https://caddyserver.com/docs/ +After=network.target network-online.target +Requires=network-online.target + +[Service] +User=caddy +Group=caddy +ExecStart=/usr/local/bin/caddy run --config /etc/caddy/Caddyfile +ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile +TimeoutStopSec=5s +LimitNOFILE=1048576 +LimitNPROC=512 +PrivateTmp=true +ProtectSystem=full +AmbientCapabilities=CAP_NET_BIND_SERVICE + +[Install] +WantedBy=multi-user.target +EOL + + success_log "Caddy wurde installiert" + fi + + # Caddyfile für HTTPS mit selbstsigniertem Zertifikat konfigurieren + log "Konfiguriere Caddy für HTTPS mit selbstsigniertem Zertifikat..." + cat > /etc/caddy/Caddyfile << EOL +{ + # Globale Optionen + admin off + auto_https disable_redirects + + # Selbstsigniertes Zertifikat verwenden + local_certs + default_sni ${FIRMENNETZWERK_HOSTNAME} +} + +# HTTPS-Konfiguration für den Firmennetzwerk-Hostnamen +${FIRMENNETZWERK_HOSTNAME} { + # TLS mit selbstsigniertem Zertifikat + tls internal { + on_demand + } + + # Reverse-Proxy zum Next.js-Frontend + reverse_proxy localhost:3000 { + # Zeitüberschreitungen für langsame Raspberry Pi-Verbindungen erhöhen + timeouts 5m + } + + # Logging + log { + output file /var/log/caddy/access.log + format console + } +} + +# HTTP-Konfiguration für lokalen Zugriff +:80 { + # Weiterleitung zu HTTPS + redir https://${FIRMENNETZWERK_HOSTNAME}{uri} permanent +} + +# Zusätzlicher Server für Backend-Proxy (API-Anfragen weiterleiten) +localhost:8000 { + reverse_proxy ${BACKEND_HOST}:${BACKEND_PORT} { + # Headers für CORS anpassen + header_up Host ${BACKEND_HOST}:${BACKEND_PORT} + header_up X-Forwarded-Host ${FIRMENNETZWERK_HOSTNAME} + header_up X-Forwarded-Proto https + + # Zeitüberschreitungen für API-Anfragen erhöhen + timeouts 1m + } + + # Logging + log { + output file /var/log/caddy/backend-access.log + format console + } +} +EOL + + # Caddy-Service neu laden und starten + log "Starte Caddy-Service..." + systemctl daemon-reload + systemctl enable caddy + systemctl restart caddy + + # Überprüfen, ob Caddy läuft + if systemctl is-active --quiet caddy; then + success_log "Caddy-Service wurde gestartet und ist aktiv" + else + error_log "Fehler beim Starten des Caddy-Services" + return 1 + fi + + return 0 +} + +# Next.js Frontend-Konfiguration für HTTPS und Backend-Proxy +configure_frontend() { + header "Frontend-Konfiguration für HTTPS" + + # Verzeichnis für das Frontend + FRONTEND_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + + # Prüfen, ob das Frontend-Verzeichnis existiert + if [ ! -d "$FRONTEND_DIR" ]; then + error_log "Frontend-Verzeichnis nicht gefunden: $FRONTEND_DIR" + return 1 + fi + + log "Konfiguriere Frontend für HTTPS und Backend-Proxy..." + + # .env.local-Datei für das Frontend erstellen + cat > "$FRONTEND_DIR/.env.local" << EOL +# Backend API Konfiguration (über lokalen Proxy zu Backend) +NEXT_PUBLIC_API_URL=http://localhost:8000 + +# Frontend-URL für OAuth Callback (HTTPS) +NEXT_PUBLIC_FRONTEND_URL=https://${FIRMENNETZWERK_HOSTNAME} + +# Explizite OAuth Callback URL für GitHub +NEXT_PUBLIC_OAUTH_CALLBACK_URL=https://${FIRMENNETZWERK_HOSTNAME}/auth/login/callback +EOL + + # Berechtigungen setzen + chown -R $(stat -c '%U:%G' "$FRONTEND_DIR") "$FRONTEND_DIR/.env.local" + chmod 600 "$FRONTEND_DIR/.env.local" + + success_log "Frontend wurde für HTTPS und Backend-Proxy konfiguriert" + + # Hinweis zur Installation und zum Start des Frontends + log "${YELLOW}Hinweis: Führen Sie nun das Frontend-Installationsskript aus:${NC}" + log "cd $FRONTEND_DIR && ./install.sh" + + return 0 +} + +# Hostname setzen +set_hostname() { + header "Hostname konfigurieren" + + log "Aktueller Hostname: $(hostname)" + log "Setze Hostname auf ${FIRMENNETZWERK_HOSTNAME}..." + + # Hostname in /etc/hostname setzen + echo "${FIRMENNETZWERK_HOSTNAME}" > /etc/hostname + + # Hostname in /etc/hosts aktualisieren + if grep -q "$(hostname)" /etc/hosts; then + sed -i "s/$(hostname)/${FIRMENNETZWERK_HOSTNAME}/g" /etc/hosts + else + echo "127.0.1.1 ${FIRMENNETZWERK_HOSTNAME}" >> /etc/hosts + fi + + # Aktualisieren des Hostnamens ohne Neustart + hostname "${FIRMENNETZWERK_HOSTNAME}" + + success_log "Hostname wurde auf ${FIRMENNETZWERK_HOSTNAME} gesetzt" + log "${YELLOW}Hinweis: Ein Neustart wird empfohlen, um sicherzustellen, dass der neue Hostname vollständig übernommen wurde.${NC}" + + return 0 +} + +# Hauptfunktion +main() { + # Begrüßung und Bestätigung + header "MYP HTTPS und Dual-Network Setup" + log "Dieses Skript richtet Ihren Raspberry Pi für das MYP-Projekt ein:" + log "- Setzt den Hostname auf: ${FIRMENNETZWERK_HOSTNAME}" + log "- Konfiguriert das duale Netzwerk (LAN für Internet, WLAN für Backend)" + log "- Installiert Caddy als Reverse-Proxy mit selbstsigniertem HTTPS" + log "- Konfiguriert das Frontend für die Kommunikation mit dem Backend" + echo "" + log "${YELLOW}WICHTIG: Diese Konfiguration kann bestehende Netzwerk- und HTTPS-Einstellungen überschreiben.${NC}" + read -p "Möchten Sie fortfahren? (j/n): " confirm + + if [[ "$confirm" != "j" ]]; then + log "Setup abgebrochen." + exit 0 + fi + + # Schritte ausführen + set_hostname || { error_log "Fehler beim Setzen des Hostnamens"; exit 1; } + setup_network || { error_log "Fehler bei der Netzwerkkonfiguration"; exit 1; } + install_caddy || { error_log "Fehler bei der Caddy-Installation"; exit 1; } + configure_frontend || { error_log "Fehler bei der Frontend-Konfiguration"; exit 1; } + + # Abschlussmeldung + header "Setup abgeschlossen" + success_log "MYP HTTPS und Dual-Network Setup erfolgreich!" + log "Ihr Raspberry Pi ist nun wie folgt konfiguriert:" + log "- Hostname: ${FIRMENNETZWERK_HOSTNAME}" + log "- HTTPS mit selbstsigniertem Zertifikat über Caddy" + log "- Duale Netzwerkkonfiguration:" + log " * eth0: Firmennetzwerk mit Internet-Zugang" + log " * wlan0: Verbindung zum Offline-Netzwerk (${OFFLINE_NETWORK_SSID})" + log "- Frontend-URL: https://${FIRMENNETZWERK_HOSTNAME}" + log "- Backend-Kommunikation über lokalen Proxy: http://localhost:8000 -> ${BACKEND_HOST}:${BACKEND_PORT}" + echo "" + log "${YELLOW}Wichtige nächste Schritte:${NC}" + log "1. Starten Sie das Frontend mit dem Installationsskript:" + log " cd $FRONTEND_DIR && ./install.sh" + log "2. Neustart des Raspberry Pi empfohlen:" + log " sudo reboot" + echo "" + log "${YELLOW}Hinweis zum selbstsignierten Zertifikat:${NC}" + log "Bei Zugriff auf https://${FIRMENNETZWERK_HOSTNAME} erhalten Sie eine Zertifikatswarnung im Browser." + log "Dies ist normal für selbstsignierte Zertifikate. Sie können die Warnung in Ihrem Browser bestätigen." +} + +# Skript starten +main \ No newline at end of file diff --git a/frontend/install.sh b/frontend/install.sh new file mode 100644 index 00000000..c33611c7 --- /dev/null +++ b/frontend/install.sh @@ -0,0 +1,570 @@ +#!/bin/bash + +# MYP Frontend Installations-Skript +# Dieses Skript installiert das Frontend mit Docker und Host-Netzwerkanbindung + +# Farbcodes für Ausgabe +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# Funktion zur Ausgabe mit Zeitstempel +log() { + echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" +} + +error_log() { + echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] FEHLER:${NC} $1" >&2 +} + +success_log() { + echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] ERFOLG:${NC} $1" +} + +header() { + echo "" + echo -e "${CYAN}===== $1 =====${NC}" + echo "" +} + +# Variablen definieren +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +FRONTEND_DIR="$SCRIPT_DIR" +DOCKER_DIR="$FRONTEND_DIR/docker" +DEFAULT_BACKEND_URL="http://192.168.0.105:5000" +IMAGE_NAME="myp-rp:latest" +CONTAINER_NAME="myp-rp" +DB_VOLUME_DIR="/srv/MYP-DB" +ENV_FILE_PATH="/srv/myp-env/github.env" + +# Prüfen, ob wir im Frontend-Verzeichnis sind +if [ ! -f "$FRONTEND_DIR/package.json" ]; then + error_log "Dieses Skript muss im Frontend-Verzeichnis ausgeführt werden." + error_log "Bitte wechseln Sie in das Frontend-Verzeichnis." + exit 1 +fi + +# Prüfen, ob Docker installiert ist +if ! command -v docker &> /dev/null; then + error_log "Docker ist nicht installiert. Bitte installieren Sie Docker zuerst." + exit 1 +fi + +# Prüfen, ob Docker läuft +if ! docker info &> /dev/null; then + error_log "Docker-Daemon läuft nicht. Bitte starten Sie Docker mit 'sudo systemctl start docker'." + exit 1 +fi + +# Prüfen, ob der Benutzer in der Docker-Gruppe ist +if ! groups | grep -q '\bdocker\b'; then + error_log "Aktueller Benutzer hat keine Docker-Berechtigungen." + error_log "Bitte führen Sie das Skript mit 'sudo' aus oder fügen Sie den Benutzer zur Docker-Gruppe hinzu:" + error_log "sudo usermod -aG docker $USER && newgrp docker" + exit 1 +fi + +# Erstelle Datenbank-Verzeichnis, falls nicht vorhanden +if [ ! -d "$DB_VOLUME_DIR" ]; then + log "Erstelle Datenbankverzeichnis: $DB_VOLUME_DIR" + if ! mkdir -p "$DB_VOLUME_DIR"; then + if ! sudo mkdir -p "$DB_VOLUME_DIR"; then + error_log "Konnte Datenbankverzeichnis nicht erstellen. Bitte erstellen Sie es manuell:" + error_log "sudo mkdir -p $DB_VOLUME_DIR && sudo chown $USER:$USER $DB_VOLUME_DIR" + exit 1 + fi + sudo chown $USER:$USER "$DB_VOLUME_DIR" + fi +else + log "Datenbankverzeichnis existiert bereits: $DB_VOLUME_DIR" +fi + +# Funktion zum Laden der Umgebungsvariablen aus /srv/myp-env/github.env +load_env_from_srv() { + if [ -f "$ENV_FILE_PATH" ]; then + log "Lade Umgebungsvariablen aus $ENV_FILE_PATH" + + # Versuche, die Variablen aus der Datei zu laden + OAUTH_CLIENT_ID=$(grep -oP 'OAUTH_CLIENT_ID=\K.*' "$ENV_FILE_PATH" 2>/dev/null || echo "client_id") + OAUTH_CLIENT_SECRET=$(grep -oP 'OAUTH_CLIENT_SECRET=\K.*' "$ENV_FILE_PATH" 2>/dev/null || echo "client_secret") + + # Prüfe, ob die Backend-URL in der Datei definiert ist + BACKEND_URL_FROM_FILE=$(grep -oP 'NEXT_PUBLIC_API_URL=\K.*' "$ENV_FILE_PATH" 2>/dev/null) + if [ -n "$BACKEND_URL_FROM_FILE" ]; then + log "Backend-URL aus $ENV_FILE_PATH geladen: $BACKEND_URL_FROM_FILE" + DEFAULT_BACKEND_URL="$BACKEND_URL_FROM_FILE" + fi + + success_log "OAuth-Konfiguration aus $ENV_FILE_PATH geladen." + else + log "${YELLOW}Warnung: $ENV_FILE_PATH nicht gefunden. Verwende Standard-Konfiguration.${NC}" + OAUTH_CLIENT_ID="client_id" + OAUTH_CLIENT_SECRET="client_secret" + fi +} + +# Funktion zum Konfigurieren der Backend-URL +configure_backend_url() { + local backend_url="${1:-$DEFAULT_BACKEND_URL}" + + header "Backend-URL konfigurieren" + log "Konfiguriere Backend-URL für Frontend: $backend_url" + + # Lade OAuth-Konfiguration aus /srv + load_env_from_srv + + # Prüfen, ob setup-backend-url.sh existiert + if [ -f "$FRONTEND_DIR/setup-backend-url.sh" ]; then + chmod +x "$FRONTEND_DIR/setup-backend-url.sh" + if ! "$FRONTEND_DIR/setup-backend-url.sh" "$backend_url"; then + error_log "Fehler beim Konfigurieren der Backend-URL." + return 1 + fi + else + # Bestimme den Hostnamen für OAuth + HOSTNAME=$(hostname) + if [[ "$HOSTNAME" == *"m040tbaraspi001"* ]] || [[ "$HOSTNAME" == *"corpintra"* ]]; then + FRONTEND_HOSTNAME="m040tbaraspi001.de040.corpintra.net" + OAUTH_URL="http://m040tbaraspi001.de040.corpintra.net/auth/login/callback" + log "Erkannt: Unternehmens-Hostname: $FRONTEND_HOSTNAME" + else + FRONTEND_HOSTNAME="$HOSTNAME" + OAUTH_URL="http://$HOSTNAME:3000/auth/login/callback" + log "Lokaler Hostname: $FRONTEND_HOSTNAME" + fi + + # Erstelle .env.local-Datei manuell + log "Erstelle .env.local-Datei manuell..." + cat > "$FRONTEND_DIR/.env.local" << EOL +# Backend API Konfiguration +NEXT_PUBLIC_API_URL=${backend_url} + +# Frontend-URL für OAuth Callback +NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME} + +# Explizite OAuth Callback URL für GitHub +NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL} + +# OAuth Konfiguration aus /srv/myp-env/github.env +OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID} +OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET} +EOL + + if [ ! -f "$FRONTEND_DIR/.env.local" ]; then + error_log "Konnte .env.local-Datei nicht erstellen." + return 1 + fi + + chmod 600 "$FRONTEND_DIR/.env.local" + fi + + success_log "Backend-URL erfolgreich konfiguriert: $backend_url" + return 0 +} + +# Funktion zum Bauen des Images +build_image() { + header "Docker-Image bauen" + log "Baue Docker-Image: $IMAGE_NAME" + + if [ ! -f "$FRONTEND_DIR/Dockerfile" ]; then + error_log "Dockerfile nicht gefunden in $FRONTEND_DIR" + return 1 + fi + + cd "$FRONTEND_DIR" || return 1 + + # Vorhandenes Image entfernen, falls gewünscht + if docker image inspect "$IMAGE_NAME" &>/dev/null; then + log "Image $IMAGE_NAME existiert bereits." + read -p "Möchten Sie das existierende Image überschreiben? (j/n): " rebuild_choice + if [[ "$rebuild_choice" == "j" ]]; then + log "Entferne existierendes Image..." + docker rmi "$IMAGE_NAME" &>/dev/null || true + else + log "Behalte existierendes Image." + return 0 + fi + fi + + # Baue das Image + log "${YELLOW}Baue Docker-Image... (Dies kann auf einem Raspberry Pi mehrere Minuten dauern)${NC}" + if ! docker build -t "$IMAGE_NAME" .; then + error_log "Fehler beim Bauen des Docker-Images." + return 1 + fi + + success_log "Docker-Image erfolgreich gebaut: $IMAGE_NAME" + return 0 +} + +# Funktion zum Speichern des Images +save_image() { + header "Docker-Image speichern" + local save_dir="${1:-$DOCKER_DIR/images}" + local save_file="$save_dir/myp-frontend.tar" + + # Prüfen, ob das Image existiert + if ! docker image inspect "$IMAGE_NAME" &>/dev/null; then + error_log "Image $IMAGE_NAME existiert nicht. Bitte bauen Sie es zuerst." + return 1 + fi + + # Verzeichnis erstellen, falls es nicht existiert + mkdir -p "$save_dir" + + log "Speichere Docker-Image in: $save_file" + log "${YELLOW}Dies kann einige Minuten dauern...${NC}" + + if ! docker save -o "$save_file" "$IMAGE_NAME"; then + error_log "Fehler beim Speichern des Docker-Images." + return 1 + fi + + # Prüfe, ob die Datei erstellt wurde + if [ ! -f "$save_file" ]; then + error_log "Konnte Docker-Image nicht speichern." + return 1 + fi + + # Prüfe Dateigröße + local filesize=$(stat -c%s "$save_file") + if [ "$filesize" -lt 1000000 ]; then # Kleiner als 1MB ist verdächtig + error_log "Gespeichertes Image ist ungewöhnlich klein ($filesize Bytes). Möglicherweise ist etwas schief gelaufen." + return 1 + fi + + success_log "Docker-Image erfolgreich gespeichert: $save_file (Größe: $(du -h "$save_file" | cut -f1))" + return 0 +} + +# Funktion zum Laden des Images +load_image() { + header "Docker-Image laden" + local load_dir="${1:-$DOCKER_DIR/images}" + local load_file="$load_dir/myp-frontend.tar" + + # Prüfen, ob die Datei existiert + if [ ! -f "$load_file" ]; then + error_log "Image-Datei nicht gefunden: $load_file" + return 1 + fi + + # Prüfe Dateigröße + local filesize=$(stat -c%s "$load_file") + if [ "$filesize" -lt 1000000 ]; then # Kleiner als 1MB ist verdächtig + error_log "Image-Datei ist ungewöhnlich klein ($filesize Bytes). Möglicherweise ist sie beschädigt." + return 1 + fi + + log "Lade Docker-Image aus: $load_file" + log "${YELLOW}Dies kann einige Minuten dauern...${NC}" + + if ! docker load -i "$load_file"; then + error_log "Fehler beim Laden des Docker-Images. Die Datei könnte beschädigt sein." + return 1 + fi + + success_log "Docker-Image erfolgreich geladen." + return 0 +} + +# Funktion zum Starten des Containers mit Docker Compose +start_container_compose() { + header "Container mit Docker Compose starten" + + # Erstellen der vereinfachten docker-compose.yml-Datei + local compose_file="$DOCKER_DIR/compose.simple.yml" + + # Lade OAuth-Konfiguration aus /srv, falls noch nicht geschehen + if [ -z "$OAUTH_CLIENT_ID" ]; then + load_env_from_srv + fi + + # Docker-Verzeichnis erstellen, falls nicht vorhanden + mkdir -p "$DOCKER_DIR" + + log "Erstelle vereinfachte Docker-Compose-Datei: $compose_file" + cat > "$compose_file" << EOL +services: + myp-rp: + image: ${IMAGE_NAME} + container_name: ${CONTAINER_NAME} + environment: + - NEXT_PUBLIC_API_URL=${BACKEND_URL} + - NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME} + - NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL} + - OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID} + - OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET} + env_file: "${ENV_FILE_PATH}" + ports: + - "3000:3000" + volumes: + - ${DB_VOLUME_DIR}:/app/db + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--spider", "http://localhost:3000"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s +EOL + + # Prüfen, ob die Datei erstellt wurde + if [ ! -f "$compose_file" ]; then + error_log "Konnte Docker-Compose-Datei nicht erstellen." + return 1 + fi + + # Stoppen des vorhandenen Containers + if docker ps -a | grep -q "$CONTAINER_NAME"; then + log "Stoppe und entferne existierenden Container..." + docker stop "$CONTAINER_NAME" &>/dev/null || true + docker rm "$CONTAINER_NAME" &>/dev/null || true + fi + + # Container starten + log "Starte Container..." + cd "$DOCKER_DIR" || return 1 + + if ! docker compose -f "$(basename "$compose_file")" up -d; then + # Versuche mit docker-compose, falls docker compose nicht funktioniert + if ! docker-compose -f "$(basename "$compose_file")" up -d; then + error_log "Fehler beim Starten des Containers." + return 1 + fi + fi + + success_log "Container erfolgreich gestartet: $CONTAINER_NAME" + return 0 +} + +# Funktion zum Starten des Containers mit Docker Run +start_container_run() { + header "Container direkt starten" + + # Stoppen des vorhandenen Containers + if docker ps -a | grep -q "$CONTAINER_NAME"; then + log "Stoppe und entferne existierenden Container..." + docker stop "$CONTAINER_NAME" &>/dev/null || true + docker rm "$CONTAINER_NAME" &>/dev/null || true + fi + + # Lade OAuth-Konfiguration aus /srv, falls noch nicht geschehen + if [ -z "$OAUTH_CLIENT_ID" ]; then + load_env_from_srv + fi + + # Container starten + log "Starte Container mit 'docker run'..." + + if ! docker run -d --name "$CONTAINER_NAME" \ + -p 3000:3000 \ + -e "NEXT_PUBLIC_API_URL=$BACKEND_URL" \ + -e "NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME}" \ + -e "NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL}" \ + -e "OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID}" \ + -e "OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET}" \ + --env-file "$ENV_FILE_PATH" \ + -v "$DB_VOLUME_DIR:/app/db" \ + --restart unless-stopped \ + "$IMAGE_NAME"; then + error_log "Fehler beim Starten des Containers." + return 1 + fi + + success_log "Container erfolgreich gestartet: $CONTAINER_NAME" + return 0 +} + +# Funktion zum Starten der Anwendung ohne Docker +start_without_docker() { + header "Anwendung ohne Docker starten" + + cd "$FRONTEND_DIR" || return 1 + + # Prüfen, ob Node.js und pnpm installiert sind + if ! command -v node &> /dev/null; then + error_log "Node.js ist nicht installiert. Bitte installieren Sie Node.js zuerst." + return 1 + fi + + if ! command -v pnpm &> /dev/null; then + log "pnpm ist nicht installiert. Installiere pnpm..." + npm install -g pnpm + if [ $? -ne 0 ]; then + error_log "Fehler beim Installieren von pnpm." + return 1 + fi + fi + + # Installiere Abhängigkeiten + log "Installiere Abhängigkeiten..." + if ! pnpm install; then + error_log "Fehler beim Installieren der Abhängigkeiten." + return 1 + fi + + # Lade OAuth-Konfiguration aus /srv und konfiguriere die Backend-URL + load_env_from_srv + if ! configure_backend_url "$BACKEND_URL"; then + error_log "Fehler beim Konfigurieren der Backend-URL." + return 1 + fi + + # Baue und starte die Anwendung + log "Baue und starte die Anwendung..." + if ! pnpm build; then + log "${YELLOW}Warnung: Build fehlgeschlagen. Versuche, im Dev-Modus zu starten...${NC}" + fi + + # Starte im Screen-Session, damit die Anwendung im Hintergrund läuft + if command -v screen &> /dev/null; then + log "Starte Anwendung in Screen-Session..." + screen -dmS myp-frontend bash -c "cd $FRONTEND_DIR && \ + NEXT_PUBLIC_API_URL=$BACKEND_URL \ + NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME} \ + NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL} \ + OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID} \ + OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET} \ + pnpm start || \ + NEXT_PUBLIC_API_URL=$BACKEND_URL \ + NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME} \ + NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL} \ + OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID} \ + OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET} \ + pnpm dev" + success_log "Anwendung im Hintergrund gestartet. Verbinden mit: screen -r myp-frontend" + else + log "${YELLOW}Screen ist nicht installiert. Starte Anwendung im Vordergrund...${NC}" + log "${YELLOW}Beenden mit Strg+C. Die Anwendung wird dann beendet.${NC}" + sleep 3 + export NEXT_PUBLIC_API_URL="$BACKEND_URL" + export NEXT_PUBLIC_FRONTEND_URL="http://${FRONTEND_HOSTNAME}" + export NEXT_PUBLIC_OAUTH_CALLBACK_URL="${OAUTH_URL}" + export OAUTH_CLIENT_ID="${OAUTH_CLIENT_ID}" + export OAUTH_CLIENT_SECRET="${OAUTH_CLIENT_SECRET}" + pnpm start || pnpm dev + fi + + return 0 +} + +# Funktion für das Hauptmenü +main_menu() { + local choice + header "MYP Frontend Deployment" + echo "Bitte wählen Sie eine Option:" + echo "" + echo "1) Alles automatisch (Build, Deploy, Starten)" + echo "2) Docker-Image bauen" + echo "3) Docker-Image speichern" + echo "4) Docker-Image laden" + echo "5) Container mit Docker Compose starten" + echo "6) Container direkt mit Docker Run starten" + echo "7) Anwendung ohne Docker starten" + echo "8) Nur Backend-URL konfigurieren" + echo "9) Beenden" + echo "" + read -p "Ihre Wahl (1-9): " choice + + case $choice in + 1) auto_deploy ;; + 2) build_image ;; + 3) save_image ;; + 4) load_image ;; + 5) configure_backend_url && start_container_compose ;; + 6) configure_backend_url && start_container_run ;; + 7) start_without_docker ;; + 8) configure_backend_url ;; + 9) log "Beende das Programm." && exit 0 ;; + *) error_log "Ungültige Auswahl. Bitte versuchen Sie es erneut." && main_menu ;; + esac + + # Zurück zum Hauptmenü, es sei denn, der Benutzer hat das Programm beendet + if [ $choice -ne 9 ]; then + read -p "Drücken Sie Enter, um zum Hauptmenü zurückzukehren..." + main_menu + fi +} + +# Automatischer Deployment-Workflow +auto_deploy() { + header "Automatisches Deployment" + log "Starte automatischen Deployment-Workflow..." + + # Konfiguriere Backend-URL + if ! configure_backend_url; then + error_log "Fehler beim Konfigurieren der Backend-URL." + return 1 + fi + + # Versuche zunächst, das Image zu laden + local load_dir="$DOCKER_DIR/images" + local load_file="$load_dir/myp-frontend.tar" + + if [ -f "$load_file" ]; then + log "Image-Datei gefunden. Versuche zu laden..." + if load_image; then + log "Image erfolgreich geladen. Überspringe Bauen." + else + log "Konnte Image nicht laden. Versuche zu bauen..." + if ! build_image; then + error_log "Automatisches Deployment fehlgeschlagen beim Bauen des Images." + return 1 + fi + fi + else + log "Keine Image-Datei gefunden. Baue neues Image..." + if ! build_image; then + error_log "Automatisches Deployment fehlgeschlagen beim Bauen des Images." + return 1 + fi + fi + + # Speichere das Image für zukünftige Verwendung + log "Speichere Image für zukünftige Verwendung..." + save_image + + # Starte den Container + log "Starte Container..." + if ! start_container_compose; then + error_log "Konnte Container nicht mit Docker Compose starten. Versuche direkten Start..." + if ! start_container_run; then + error_log "Automatisches Deployment fehlgeschlagen beim Starten des Containers." + return 1 + fi + fi + + success_log "Automatisches Deployment erfolgreich abgeschlossen!" + log "Frontend ist unter http://localhost:3000 erreichbar" + log "API-Kommunikation mit Backend: $BACKEND_URL" + + return 0 +} + +# Hauptanwendung +# Zuerst nach Backend-URL fragen +header "Backend-URL Konfiguration" +log "Standard-Backend-URL: $DEFAULT_BACKEND_URL" +read -p "Möchten Sie eine andere Backend-URL verwenden? (j/n): " change_url_choice + +if [[ "$change_url_choice" == "j" ]]; then + read -p "Geben Sie die neue Backend-URL ein (z.B. http://192.168.0.105:5000): " custom_url + if [ -n "$custom_url" ]; then + BACKEND_URL="$custom_url" + log "Verwende benutzerdefinierte Backend-URL: $BACKEND_URL" + else + BACKEND_URL="$DEFAULT_BACKEND_URL" + log "Leere Eingabe. Verwende Standard-Backend-URL: $BACKEND_URL" + fi +else + BACKEND_URL="$DEFAULT_BACKEND_URL" + log "Verwende Standard-Backend-URL: $BACKEND_URL" +fi + +# Anzeigen des Hauptmenüs +main_menu \ No newline at end of file diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs new file mode 100644 index 00000000..887b2d21 --- /dev/null +++ b/frontend/next.config.mjs @@ -0,0 +1,26 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + async headers() { + return [ + { + source: "/:path*", + headers: [ + { + key: "Access-Control-Allow-Origin", + value: "m040tbaraspi001.de040.corpintra.net", + }, + { + key: "Access-Control-Allow-Methods", + value: "GET, POST, PUT, DELETE, OPTIONS", + }, + { + key: "Access-Control-Allow-Headers", + value: "Content-Type, Authorization", + }, + ], + }, + ]; + }, +}; + +export default nextConfig; diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..23f19656 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,83 @@ +{ + "name": "myp-rp", + "version": "1.0.0", + "private": true, + "packageManager": "pnpm@9.12.1", + "scripts": { + "dev": "next dev", + "build": "node update-package.js && next build", + "start": "next start", + "lint": "next lint", + "db:create-default": "mkdir -p db/", + "db:generate-sqlite": "pnpm drizzle-kit generate", + "db:clean": "rm -rf db/ drizzle/", + "db:migrate": "pnpm drizzle-kit migrate", + "db": "pnpm db:create-default && pnpm db:generate-sqlite && pnpm db:migrate", + "db:reset": "pnpm db:clean && pnpm db" + }, + "dependencies": { + "@faker-js/faker": "^9.2.0", + "@headlessui/react": "^2.1.10", + "@headlessui/tailwindcss": "^0.2.1", + "@hookform/resolvers": "^3.9.0", + "@libsql/client": "^0.14.0", + "@lucia-auth/adapter-drizzle": "^1.1.0", + "@radix-ui/react-alert-dialog": "^1.1.2", + "@radix-ui/react-avatar": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dropdown-menu": "^2.1.2", + "@radix-ui/react-hover-card": "^1.1.2", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-scroll-area": "^1.2.0", + "@radix-ui/react-select": "^2.1.2", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.1", + "@radix-ui/react-toast": "^1.2.2", + "@remixicon/react": "^4.3.0", + "@tanstack/react-table": "^8.20.5", + "@tremor/react": "^3.18.3", + "arctic": "^1.9.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "drizzle-orm": "^0.30.10", + "lodash": "^4.17.21", + "lucia": "^3.2.1", + "lucide-react": "^0.378.0", + "luxon": "^3.5.0", + "next": "14.2.3", + "next-themes": "^0.3.0", + "oslo": "^1.2.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.53.0", + "react-if": "^4.1.5", + "react-timer-hook": "^3.0.7", + "recharts": "^2.13.3", + "regression": "^2.0.1", + "sonner": "^1.5.0", + "sqlite": "^5.1.1", + "sqlite3": "^5.1.7", + "swr": "^2.2.5", + "tailwind-merge": "^2.5.3", + "tailwindcss-animate": "^1.0.7", + "use-debounce": "^10.0.3", + "uuid": "^11.0.2", + "zod": "^3.23.8" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.3", + "@tailwindcss/forms": "^0.5.9", + "@types/lodash": "^4.17.13", + "@types/luxon": "^3.4.2", + "@types/node": "^20.16.11", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", + "drizzle-kit": "^0.21.4", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.13", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + } +} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 00000000..cef9b75a --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,5707 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@faker-js/faker': + specifier: ^9.2.0 + version: 9.2.0 + '@headlessui/react': + specifier: ^2.1.10 + version: 2.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@headlessui/tailwindcss': + specifier: ^0.2.1 + version: 0.2.1(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3))) + '@hookform/resolvers': + specifier: ^3.9.0 + version: 3.9.0(react-hook-form@7.53.0(react@18.3.1)) + '@libsql/client': + specifier: ^0.14.0 + version: 0.14.0 + '@lucia-auth/adapter-drizzle': + specifier: ^1.1.0 + version: 1.1.0(drizzle-orm@0.30.10(@libsql/client@0.14.0)(@types/better-sqlite3@7.6.11)(@types/react@18.3.11)(better-sqlite3@9.6.0)(react@18.3.1)(sqlite3@5.1.7))(lucia@3.2.1) + '@radix-ui/react-alert-dialog': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-avatar': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.2 + version: 2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-hover-card': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-icons': + specifier: ^1.3.0 + version: 1.3.0(react@18.3.1) + '@radix-ui/react-label': + specifier: ^2.1.0 + version: 2.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-scroll-area': + specifier: ^1.2.0 + version: 1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': + specifier: ^2.1.2 + version: 2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': + specifier: ^1.1.0 + version: 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-tabs': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toast': + specifier: ^1.2.2 + version: 1.2.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@remixicon/react': + specifier: ^4.3.0 + version: 4.3.0(react@18.3.1) + '@tanstack/react-table': + specifier: ^8.20.5 + version: 8.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tremor/react': + specifier: ^3.18.3 + version: 3.18.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3))) + arctic: + specifier: ^1.9.2 + version: 1.9.2 + class-variance-authority: + specifier: ^0.7.0 + version: 0.7.0 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + drizzle-orm: + specifier: ^0.30.10 + version: 0.30.10(@libsql/client@0.14.0)(@types/better-sqlite3@7.6.11)(@types/react@18.3.11)(better-sqlite3@9.6.0)(react@18.3.1)(sqlite3@5.1.7) + lodash: + specifier: ^4.17.21 + version: 4.17.21 + lucia: + specifier: ^3.2.1 + version: 3.2.1 + lucide-react: + specifier: ^0.378.0 + version: 0.378.0(react@18.3.1) + luxon: + specifier: ^3.5.0 + version: 3.5.0 + next: + specifier: 14.2.3 + version: 14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-themes: + specifier: ^0.3.0 + version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + oslo: + specifier: ^1.2.1 + version: 1.2.1 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + react-hook-form: + specifier: ^7.53.0 + version: 7.53.0(react@18.3.1) + react-if: + specifier: ^4.1.5 + version: 4.1.5(react@18.3.1) + react-timer-hook: + specifier: ^3.0.7 + version: 3.0.7(react@18.3.1) + recharts: + specifier: ^2.13.3 + version: 2.13.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + regression: + specifier: ^2.0.1 + version: 2.0.1 + sonner: + specifier: ^1.5.0 + version: 1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + sqlite: + specifier: ^5.1.1 + version: 5.1.1 + sqlite3: + specifier: ^5.1.7 + version: 5.1.7 + swr: + specifier: ^2.2.5 + version: 2.2.5(react@18.3.1) + tailwind-merge: + specifier: ^2.5.3 + version: 2.5.3 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3))) + use-debounce: + specifier: ^10.0.3 + version: 10.0.3(react@18.3.1) + uuid: + specifier: ^11.0.2 + version: 11.0.2 + zod: + specifier: ^3.23.8 + version: 3.23.8 + devDependencies: + '@biomejs/biome': + specifier: ^1.9.3 + version: 1.9.3 + '@tailwindcss/forms': + specifier: ^0.5.9 + version: 0.5.9(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3))) + '@types/lodash': + specifier: ^4.17.13 + version: 4.17.13 + '@types/luxon': + specifier: ^3.4.2 + version: 3.4.2 + '@types/node': + specifier: ^20.16.11 + version: 20.16.11 + '@types/react': + specifier: ^18.3.11 + version: 18.3.11 + '@types/react-dom': + specifier: ^18.3.1 + version: 18.3.1 + drizzle-kit: + specifier: ^0.21.4 + version: 0.21.4 + postcss: + specifier: ^8.4.47 + version: 8.4.47 + tailwindcss: + specifier: ^3.4.13 + version: 3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3)) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.16.11)(typescript@5.6.3) + typescript: + specifier: ^5.6.3 + version: 5.6.3 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/runtime@7.24.5': + resolution: {integrity: sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@1.9.3': + resolution: {integrity: sha512-POjAPz0APAmX33WOQFGQrwLvlu7WLV4CFJMlB12b6ZSg+2q6fYu9kZwLCOA+x83zXfcPd1RpuWOKJW0GbBwLIQ==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@1.9.3': + resolution: {integrity: sha512-QZzD2XrjJDUyIZK+aR2i5DDxCJfdwiYbUKu9GzkCUJpL78uSelAHAPy7m0GuPMVtF/Uo+OKv97W3P9nuWZangQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@1.9.3': + resolution: {integrity: sha512-vSCoIBJE0BN3SWDFuAY/tRavpUtNoqiceJ5PrU3xDfsLcm/U6N93JSM0M9OAiC/X7mPPfejtr6Yc9vSgWlEgVw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@1.9.3': + resolution: {integrity: sha512-VBzyhaqqqwP3bAkkBrhVq50i3Uj9+RWuj+pYmXrMDgjS5+SKYGE56BwNw4l8hR3SmYbLSbEo15GcV043CDSk+Q==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@1.9.3': + resolution: {integrity: sha512-vJkAimD2+sVviNTbaWOGqEBy31cW0ZB52KtpVIbkuma7PlfII3tsLhFa+cwbRAcRBkobBBhqZ06hXoZAN8NODQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@1.9.3': + resolution: {integrity: sha512-TJmnOG2+NOGM72mlczEsNki9UT+XAsMFAOo8J0me/N47EJ/vkLXxf481evfHLlxMejTY6IN8SdRSiPVLv6AHlA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@1.9.3': + resolution: {integrity: sha512-x220V4c+romd26Mu1ptU+EudMXVS4xmzKxPVb9mgnfYlN4Yx9vD5NZraSx/onJnd3Gh/y8iPUdU5CDZJKg9COA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@1.9.3': + resolution: {integrity: sha512-lg/yZis2HdQGsycUvHWSzo9kOvnGgvtrYRgoCEwPBwwAL8/6crOp3+f47tPwI/LI1dZrhSji7PNsGKGHbwyAhw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@1.9.3': + resolution: {integrity: sha512-cQMy2zanBkVLpmmxXdK6YePzmZx0s5Z7KEnwmrW54rcXK3myCNbQa09SwGZ8i/8sLw0H9F3X7K4rxVNGU8/D4Q==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/core@0.45.0': + resolution: {integrity: sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==} + + '@emnapi/runtime@0.45.0': + resolution: {integrity: sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild/aix-ppc64@0.19.12': + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.19.12': + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.19.12': + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.19.12': + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.19.12': + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.19.12': + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.19.12': + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.19.12': + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.19.12': + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.19.12': + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.19.12': + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.19.12': + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.19.12': + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.19.12': + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.19.12': + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.19.12': + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.19.12': + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.19.12': + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.19.12': + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.19.12': + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.19.12': + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.19.12': + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.19.12': + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@faker-js/faker@9.2.0': + resolution: {integrity: sha512-ulqQu4KMr1/sTFIYvqSdegHT8NIkt66tFAkugGnHA+1WAfEn6hMzNR+svjXGFRVLnapxvej67Z/LwchFrnLBUg==} + engines: {node: '>=18.0.0', npm: '>=9.0.0'} + + '@floating-ui/core@1.6.1': + resolution: {integrity: sha512-42UH54oPZHPdRHdw6BgoBD6cg/eVTmVrFcgeRDM3jbO7uxSoipVcmcIGFcA5jmOHO5apcyvBhkSKES3fQJnu7A==} + + '@floating-ui/dom@1.6.5': + resolution: {integrity: sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==} + + '@floating-ui/react-dom@1.3.0': + resolution: {integrity: sha512-htwHm67Ji5E/pROEAr7f8IKFShuiCKHwUC/UY4vC3I5jiSvGFAYnSYiZO5MlGmads+QqvUkR9ANHEguGrDv72g==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react-dom@2.0.9': + resolution: {integrity: sha512-q0umO0+LQK4+p6aGyvzASqKbKOJcAHJ7ycE9CuUvfx3s9zTHWmGJTPOIlM/hmSBfUfg/XfY5YhLBLR/LHwShQQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react-dom@2.1.2': + resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.19.2': + resolution: {integrity: sha512-JyNk4A0Ezirq8FlXECvRtQOX/iBe5Ize0W/pLkrZjfHW9GUV7Xnq6zm6fyZuQzaHHqEnVizmvlA96e1/CkZv+w==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.26.24': + resolution: {integrity: sha512-2ly0pCkZIGEQUq5H8bBK0XJmc1xIK/RM3tvVzY3GBER7IOD1UgmC2Y2tjj4AuS+TC+vTE1KJv2053290jua0Sw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.8': + resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==} + + '@gar/promisify@1.1.3': + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + + '@headlessui/react@1.7.19': + resolution: {integrity: sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==} + engines: {node: '>=10'} + peerDependencies: + react: ^16 || ^17 || ^18 + react-dom: ^16 || ^17 || ^18 + + '@headlessui/react@2.1.10': + resolution: {integrity: sha512-6mLa2fjMDAFQi+/R10B+zU3edsUk/MDtENB2zHho0lqKU1uzhAfJLUduWds4nCo8wbl3vULtC5rJfZAQ1yqIng==} + engines: {node: '>=10'} + peerDependencies: + react: ^18 + react-dom: ^18 + + '@headlessui/tailwindcss@0.2.1': + resolution: {integrity: sha512-2+5+NZ+RzMyrVeCZOxdbvkUSssSxGvcUxphkIfSVLpRiKsj+/63T2TOL9dBYMXVfj/CGr6hMxSRInzXv6YY7sA==} + engines: {node: '>=10'} + peerDependencies: + tailwindcss: ^3.0 + + '@hookform/resolvers@3.9.0': + resolution: {integrity: sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==} + peerDependencies: + react-hook-form: ^7.0.0 + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.4.15': + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@libsql/client@0.14.0': + resolution: {integrity: sha512-/9HEKfn6fwXB5aTEEoMeFh4CtG0ZzbncBb1e++OCdVpgKZ/xyMsIVYXm0w7Pv4RUel803vE6LwniB3PqD72R0Q==} + + '@libsql/core@0.14.0': + resolution: {integrity: sha512-nhbuXf7GP3PSZgdCY2Ecj8vz187ptHlZQ0VRc751oB2C1W8jQUXKKklvt7t1LJiUTQBVJuadF628eUk+3cRi4Q==} + + '@libsql/darwin-arm64@0.4.6': + resolution: {integrity: sha512-45i604CJ2Lubbg7NqtDodjarF6VgST8rS5R8xB++MoRqixtDns9PZ6tocT9pRJDWuTWEiy2sjthPOFWMKwYAsg==} + cpu: [arm64] + os: [darwin] + + '@libsql/darwin-x64@0.4.6': + resolution: {integrity: sha512-dRKliflhfr5zOPSNgNJ6C2nZDd4YA8bTXF3MUNqNkcxQ8BffaH9uUwL9kMq89LkFIZQHcyP75bBgZctxfJ/H5Q==} + cpu: [x64] + os: [darwin] + + '@libsql/hrana-client@0.7.0': + resolution: {integrity: sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw==} + + '@libsql/isomorphic-fetch@0.3.1': + resolution: {integrity: sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw==} + engines: {node: '>=18.0.0'} + + '@libsql/isomorphic-ws@0.1.5': + resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==} + + '@libsql/linux-arm64-gnu@0.4.6': + resolution: {integrity: sha512-DMPavVyY6vYPAYcQR1iOotHszg+5xSjHSg6F9kNecPX0KKdGq84zuPJmORfKOPtaWvzPewNFdML/e+s1fu09XQ==} + cpu: [arm64] + os: [linux] + + '@libsql/linux-arm64-musl@0.4.6': + resolution: {integrity: sha512-whuHSYAZyclGjM3L0mKGXyWqdAy7qYvPPn+J1ve7FtGkFlM0DiIPjA5K30aWSGJSRh72sD9DBZfnu8CpfSjT6w==} + cpu: [arm64] + os: [linux] + + '@libsql/linux-x64-gnu@0.4.6': + resolution: {integrity: sha512-0ggx+5RwEbYabIlDBBAvavdfIJCZ757u6nDZtBeQIhzW99EKbWG3lvkXHM3qudFb/pDWSUY4RFBm6vVtF1cJGA==} + cpu: [x64] + os: [linux] + + '@libsql/linux-x64-musl@0.4.6': + resolution: {integrity: sha512-SWNrv7Hz72QWlbM/ZsbL35MPopZavqCUmQz2HNDZ55t0F+kt8pXuP+bbI2KvmaQ7wdsoqAA4qBmjol0+bh4ndw==} + cpu: [x64] + os: [linux] + + '@libsql/win32-x64-msvc@0.4.6': + resolution: {integrity: sha512-Q0axn110zDNELfkEog3Nl8p9BU4eI/UvgaHevGyOiSDN7s0KPfj0j6jwVHk4oz3o/d/Gg3DRIxomZ4ftfTOy/g==} + cpu: [x64] + os: [win32] + + '@lucia-auth/adapter-drizzle@1.1.0': + resolution: {integrity: sha512-iCTnZWvfI5lLZOdUHZYiXA1jaspIFEeo2extLxQ3DjP3uOVys7IPwBi7zezLIRu9dhro4H4Kji+7gSYyjcef2A==} + peerDependencies: + drizzle-orm: '>= 0.29 <1' + lucia: 3.x + + '@neon-rs/load@0.0.4': + resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} + + '@next/env@14.2.3': + resolution: {integrity: sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==} + + '@next/swc-darwin-arm64@14.2.3': + resolution: {integrity: sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@14.2.3': + resolution: {integrity: sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@14.2.3': + resolution: {integrity: sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@14.2.3': + resolution: {integrity: sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@14.2.3': + resolution: {integrity: sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@14.2.3': + resolution: {integrity: sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@14.2.3': + resolution: {integrity: sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-ia32-msvc@14.2.3': + resolution: {integrity: sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@next/swc-win32-x64-msvc@14.2.3': + resolution: {integrity: sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@node-rs/argon2-android-arm-eabi@1.7.0': + resolution: {integrity: sha512-udDqkr5P9E+wYX1SZwAVPdyfYvaF4ry9Tm+R9LkfSHbzWH0uhU6zjIwNRp7m+n4gx691rk+lqqDAIP8RLKwbhg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@node-rs/argon2-android-arm64@1.7.0': + resolution: {integrity: sha512-s9j/G30xKUx8WU50WIhF0fIl1EdhBGq0RQ06lEhZ0Gi0ap8lhqbE2Bn5h3/G2D1k0Dx+yjeVVNmt/xOQIRG38A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@node-rs/argon2-darwin-arm64@1.7.0': + resolution: {integrity: sha512-ZIz4L6HGOB9U1kW23g+m7anGNuTZ0RuTw0vNp3o+2DWpb8u8rODq6A8tH4JRL79S+Co/Nq608m9uackN2pe0Rw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@node-rs/argon2-darwin-x64@1.7.0': + resolution: {integrity: sha512-5oi/pxqVhODW/pj1+3zElMTn/YukQeywPHHYDbcAW3KsojFjKySfhcJMd1DjKTc+CHQI+4lOxZzSUzK7mI14Hw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@node-rs/argon2-freebsd-x64@1.7.0': + resolution: {integrity: sha512-Ify08683hA4QVXYoIm5SUWOY5DPIT/CMB0CQT+IdxQAg/F+qp342+lUkeAtD5bvStQuCx/dFO3bnnzoe2clMhA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@node-rs/argon2-linux-arm-gnueabihf@1.7.0': + resolution: {integrity: sha512-7DjDZ1h5AUHAtRNjD19RnQatbhL+uuxBASuuXIBu4/w6Dx8n7YPxwTP4MXfsvuRgKuMWiOb/Ub/HJ3kXVCXRkg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@node-rs/argon2-linux-arm64-gnu@1.7.0': + resolution: {integrity: sha512-nJDoMP4Y3YcqGswE4DvP080w6O24RmnFEDnL0emdI8Nou17kNYBzP2546Nasx9GCyLzRcYQwZOUjrtUuQ+od2g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@node-rs/argon2-linux-arm64-musl@1.7.0': + resolution: {integrity: sha512-BKWS8iVconhE3jrb9mj6t1J9vwUqQPpzCbUKxfTGJfc+kNL58F1SXHBoe2cDYGnHrFEHTY0YochzXoAfm4Dm/A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@node-rs/argon2-linux-x64-gnu@1.7.0': + resolution: {integrity: sha512-EmgqZOlf4Jurk/szW1iTsVISx25bKksVC5uttJDUloTgsAgIGReCpUUO1R24pBhu9ESJa47iv8NSf3yAfGv6jQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@node-rs/argon2-linux-x64-musl@1.7.0': + resolution: {integrity: sha512-/o1efYCYIxjfuoRYyBTi2Iy+1iFfhqHCvvVsnjNSgO1xWiWrX0Rrt/xXW5Zsl7vS2Y+yu8PL8KFWRzZhaVxfKA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@node-rs/argon2-wasm32-wasi@1.7.0': + resolution: {integrity: sha512-Evmk9VcxqnuwQftfAfYEr6YZYSPLzmKUsbFIMep5nTt9PT4XYRFAERj7wNYp+rOcBenF3X4xoB+LhwcOMTNE5w==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@node-rs/argon2-win32-arm64-msvc@1.7.0': + resolution: {integrity: sha512-qgsU7T004COWWpSA0tppDqDxbPLgg8FaU09krIJ7FBl71Sz8SFO40h7fDIjfbTT5w7u6mcaINMQ5bSHu75PCaA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@node-rs/argon2-win32-ia32-msvc@1.7.0': + resolution: {integrity: sha512-JGafwWYQ/HpZ3XSwP4adQ6W41pRvhcdXvpzIWtKvX+17+xEXAe2nmGWM6s27pVkg1iV2ZtoYLRDkOUoGqZkCcg==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@node-rs/argon2-win32-x64-msvc@1.7.0': + resolution: {integrity: sha512-9oq4ShyFakw8AG3mRls0AoCpxBFcimYx7+jvXeAf2OqKNO+mSA6eZ9z7KQeVCi0+SOEUYxMGf5UiGiDb9R6+9Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@node-rs/argon2@1.7.0': + resolution: {integrity: sha512-zfULc+/tmcWcxn+nHkbyY8vP3+MpEqKORbszt4UkpqZgBgDAAIYvuDN/zukfTgdmo6tmJKKVfzigZOPk4LlIog==} + engines: {node: '>= 10'} + + '@node-rs/bcrypt-android-arm-eabi@1.9.0': + resolution: {integrity: sha512-nOCFISGtnodGHNiLrG0WYLWr81qQzZKYfmwHc7muUeq+KY0sQXyHOwZk9OuNQAWv/lnntmtbwkwT0QNEmOyLvA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@node-rs/bcrypt-android-arm64@1.9.0': + resolution: {integrity: sha512-+ZrIAtigVmjYkqZQTThHVlz0+TG6D+GDHWhVKvR2DifjtqJ0i+mb9gjo++hN+fWEQdWNGxKCiBBjwgT4EcXd6A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@node-rs/bcrypt-darwin-arm64@1.9.0': + resolution: {integrity: sha512-CQiS+F9Pa0XozvkXR1g7uXE9QvBOPOplDg0iCCPRYTN9PqA5qYxhwe48G3o+v2UeQceNRrbnEtWuANm7JRqIhw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@node-rs/bcrypt-darwin-x64@1.9.0': + resolution: {integrity: sha512-4pTKGawYd7sNEjdJ7R/R67uwQH1VvwPZ0SSUMmeNHbxD5QlwAPXdDH11q22uzVXsvNFZ6nGQBg8No5OUGpx6Ug==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@node-rs/bcrypt-freebsd-x64@1.9.0': + resolution: {integrity: sha512-UmWzySX4BJhT/B8xmTru6iFif3h0Rpx3TqxRLCcbgmH43r7k5/9QuhpiyzpvKGpKHJCFNm4F3rC2wghvw5FCIg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@node-rs/bcrypt-linux-arm-gnueabihf@1.9.0': + resolution: {integrity: sha512-8qoX4PgBND2cVwsbajoAWo3NwdfJPEXgpCsZQZURz42oMjbGyhhSYbovBCskGU3EBLoC8RA2B1jFWooeYVn5BA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@node-rs/bcrypt-linux-arm64-gnu@1.9.0': + resolution: {integrity: sha512-TuAC6kx0SbcIA4mSEWPi+OCcDjTQUMl213v5gMNlttF+D4ieIZx6pPDGTaMO6M2PDHTeCG0CBzZl0Lu+9b0c7Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@node-rs/bcrypt-linux-arm64-musl@1.9.0': + resolution: {integrity: sha512-/sIvKDABOI8QOEnLD7hIj02BVaNOuCIWBKvxcJOt8+TuwJ6zmY1UI5kSv9d99WbiHjTp97wtAUbZQwauU4b9ew==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@node-rs/bcrypt-linux-x64-gnu@1.9.0': + resolution: {integrity: sha512-DyyhDHDsLBsCKz1tZ1hLvUZSc1DK0FU0v52jK6IBQxrj24WscSU9zZe7ie/V9kdmA4Ep57BfpWX8Dsa2JxGdgQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@node-rs/bcrypt-linux-x64-musl@1.9.0': + resolution: {integrity: sha512-duIiuqQ+Lew8ASSAYm6ZRqcmfBGWwsi81XLUwz86a2HR7Qv6V4yc3ZAUQovAikhjCsIqe8C11JlAZSK6+PlXYg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@node-rs/bcrypt-wasm32-wasi@1.9.0': + resolution: {integrity: sha512-ylaGmn9Wjwv/D5lxtawttx3H6Uu2WTTR7lWlRHGT6Ga/MB1Vj4OjSGUW8G8zIVnKuXpGbZ92pgHlt4HUpSLctw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@node-rs/bcrypt-win32-arm64-msvc@1.9.0': + resolution: {integrity: sha512-2h86gF7QFyEzODuDFml/Dp1MSJoZjxJ4yyT2Erf4NkwsiA5MqowUhUsorRwZhX6+2CtlGa7orbwi13AKMsYndw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@node-rs/bcrypt-win32-ia32-msvc@1.9.0': + resolution: {integrity: sha512-kqxalCvhs4FkN0+gWWfa4Bdy2NQAkfiqq/CEf6mNXC13RSV673Ev9V8sRlQyNpCHCNkeXfOT9pgoBdJmMs9muA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@node-rs/bcrypt-win32-x64-msvc@1.9.0': + resolution: {integrity: sha512-2y0Tuo6ZAT2Cz8V7DHulSlv1Bip3zbzeXyeur+uR25IRNYXKvI/P99Zl85Fbuu/zzYAZRLLlGTRe6/9IHofe/w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@node-rs/bcrypt@1.9.0': + resolution: {integrity: sha512-u2OlIxW264bFUfvbFqDz9HZKFjwe8FHFtn7T/U8mYjPZ7DWYpbUB+/dkW/QgYfMSfR0ejkyuWaBBe0coW7/7ig==} + engines: {node: '>= 10'} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@npmcli/fs@1.1.1': + resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} + + '@npmcli/move-file@1.1.2': + resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} + engines: {node: '>=10'} + deprecated: This functionality has been moved to @npmcli/fs + + '@oslojs/asn1@1.0.0': + resolution: {integrity: sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==} + + '@oslojs/binary@1.0.0': + resolution: {integrity: sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==} + + '@oslojs/crypto@1.0.1': + resolution: {integrity: sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ==} + + '@oslojs/encoding@1.1.0': + resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@radix-ui/number@1.1.0': + resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} + + '@radix-ui/primitive@1.1.0': + resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} + + '@radix-ui/react-alert-dialog@1.1.2': + resolution: {integrity: sha512-eGSlLzPhKO+TErxkiGcCZGuvbVMnLA1MTnyBksGOeGRGkxHiiJUujsjmNTdWTm4iHVSRaUao9/4Ur671auMghQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.0': + resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.1': + resolution: {integrity: sha512-eoOtThOmxeoizxpX6RiEsQZ2wj5r4+zoeqAwO0cBaFQGjJwIH3dIX0OCxNrCyrrdxG+vBweMETh3VziQG7c1kw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.0': + resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.0': + resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.0': + resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.1': + resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.2': + resolution: {integrity: sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.0': + resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.1': + resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.2': + resolution: {integrity: sha512-GVZMR+eqK8/Kes0a36Qrv+i20bAPXSn8rCBTHx30w+3ECnR5o3xixAlqcVaYvLeyKUsm0aqyhWfmUcqufM8nYA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.1': + resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.0': + resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.2': + resolution: {integrity: sha512-Y5w0qGhysvmqsIy6nQxaPa6mXNKznfoGjOfBgzOjocLxr2XlSjqBMYQQL+FfyogsMuX+m8cZyQGYhJxvxUzO4w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-icons@1.3.0': + resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==} + peerDependencies: + react: ^16.x || ^17.x || ^18.x + + '@radix-ui/react-id@1.1.0': + resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.0': + resolution: {integrity: sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.2': + resolution: {integrity: sha512-lZ0R4qR2Al6fZ4yCCZzu/ReTFrylHFxIqy7OezIpWF4bL0o9biKo0pFIvkaew3TyZ9Fy5gYVrR5zCGZBVbO1zg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.0': + resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.2': + resolution: {integrity: sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.1': + resolution: {integrity: sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.0.0': + resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.0': + resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.0': + resolution: {integrity: sha512-q2jMBdsJ9zB7QG6ngQNzNwlvxLQqONyL58QbEGwuyRZZb/ARQwk3uQVbCF7GvQVOtV6EU/pDxAw3zRzJZI3rpQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.1.2': + resolution: {integrity: sha512-rZJtWmorC7dFRi0owDmoijm6nSJH1tVw64QGiNIZ9PNLyBDtG+iAq+XGsya052At4BfarzY/Dhv9wrrUr6IMZA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.1.0': + resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-tabs@1.1.1': + resolution: {integrity: sha512-3GBUDmP2DvzmtYLMsHmpA1GtR46ZDZ+OreXM/N+kkQJOPIgytFWWTfDQmBQKBvaFS0Vno0FktdbVzN28KGrMdw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toast@1.2.2': + resolution: {integrity: sha512-Z6pqSzmAP/bFJoqMAston4eSNa+ud44NSZTiZUmUen+IOZ5nBY8kzuU5WDBVyFXPtcW6yUalOHsxM/BP6Sv8ww==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.0': + resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.1.0': + resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.0': + resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.0': + resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.0': + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.0': + resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.0': + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.1.0': + resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.0': + resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + + '@react-aria/focus@3.18.3': + resolution: {integrity: sha512-WKUElg+5zS0D3xlVn8MntNnkzJql2J6MuzAMP8Sv5WTgFDse/XGR842dsxPTIyKKdrWVCRegCuwa4m3n/GzgJw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + + '@react-aria/interactions@3.22.3': + resolution: {integrity: sha512-RRUb/aG+P0IKTIWikY/SylB6bIbLZeztnZY2vbe7RAG5MgVaCgn5HQ45SI15GlTmhsFG8CnF6slJsUFJiNHpbQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + + '@react-aria/ssr@3.9.6': + resolution: {integrity: sha512-iLo82l82ilMiVGy342SELjshuWottlb5+VefO3jOQqQRNYnJBFpUSadswDPbRimSgJUZuFwIEYs6AabkP038fA==} + engines: {node: '>= 12'} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + + '@react-aria/utils@3.25.3': + resolution: {integrity: sha512-PR5H/2vaD8fSq0H/UB9inNbc8KDcVmW6fYAfSWkkn+OAdhTTMVKqXXrZuZBWyFfSD5Ze7VN6acr4hrOQm2bmrA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + + '@react-stately/utils@3.10.4': + resolution: {integrity: sha512-gBEQEIMRh5f60KCm7QKQ2WfvhB2gLUr9b72sqUdIZ2EG+xuPgaIlCBeSicvjmjBvYZwOjoOEnmIkcx2GHp/HWw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + + '@react-types/shared@3.25.0': + resolution: {integrity: sha512-OZSyhzU6vTdW3eV/mz5i6hQwQUhkRs7xwY2d1aqPvTdMe0+2cY7Fwp45PAiwYLEj73i9ro2FxF9qC4DvHGSCgQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + + '@remixicon/react@4.3.0': + resolution: {integrity: sha512-mAVDn8pAa9dURltGwiYrf7bPIqjG4ZAnCUHfjpgz3g+HLSDNXOaJ67Z5wmjVB5KMGpp9JbbTN5vsp2z+ajVLWg==} + peerDependencies: + react: '>=18.2.0' + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.5': + resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + + '@tailwindcss/forms@0.5.9': + resolution: {integrity: sha512-tM4XVr2+UVTxXJzey9Twx48c1gcxFStqn1pQz0tRsX8o3DvxhN5oY5pvyAbUx7VTaZxpej4Zzvc6h+1RJBzpIg==} + peerDependencies: + tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20' + + '@tanstack/react-table@8.20.5': + resolution: {integrity: sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/react-virtual@3.10.8': + resolution: {integrity: sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@tanstack/react-virtual@3.5.0': + resolution: {integrity: sha512-rtvo7KwuIvqK9zb0VZ5IL7fiJAEnG+0EiFZz8FUOs+2mhGqdGmjKIaT1XU7Zq0eFqL0jonLlhbayJI/J2SA/Bw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@tanstack/table-core@8.20.5': + resolution: {integrity: sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==} + engines: {node: '>=12'} + + '@tanstack/virtual-core@3.10.8': + resolution: {integrity: sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==} + + '@tanstack/virtual-core@3.5.0': + resolution: {integrity: sha512-KnPRCkQTyqhanNC0K63GBG3wA8I+D1fQuVnAvcBF8f13akOKeQp1gSbu6f77zCxhEk727iV5oQnbHLYzHrECLg==} + + '@tootallnate/once@1.1.2': + resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} + engines: {node: '>= 6'} + + '@tremor/react@3.18.3': + resolution: {integrity: sha512-7QyGE2W9f2FpwH24TKy3/mqBgLl4sHZeQcXP3rxXZ8W2AUq7AVaG1+vIT3xXxISrkh7zknjWlZsuhoF8NWNVDw==} + peerDependencies: + react: ^18.0.0 + react-dom: '>=16.6.0' + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@tybys/wasm-util@0.8.3': + resolution: {integrity: sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==} + + '@types/better-sqlite3@7.6.11': + resolution: {integrity: sha512-i8KcD3PgGtGBLl3+mMYA8PdKkButvPyARxA7IQAd6qeslht13qxb1zzO8dRCtE7U3IoJS782zDBAeoKiM695kg==} + + '@types/d3-array@3.2.1': + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.0': + resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==} + + '@types/d3-scale@4.0.8': + resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==} + + '@types/d3-shape@3.1.6': + resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==} + + '@types/d3-time@3.0.3': + resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/lodash@4.17.13': + resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==} + + '@types/luxon@3.4.2': + resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} + + '@types/node@20.16.11': + resolution: {integrity: sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==} + + '@types/prop-types@15.7.12': + resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + + '@types/react-dom@18.3.1': + resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==} + + '@types/react@18.3.11': + resolution: {integrity: sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==} + + '@types/ws@8.5.12': + resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agentkeepalive@4.5.0: + resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} + engines: {node: '>= 8.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + aproba@2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + + arctic@1.9.2: + resolution: {integrity: sha512-VTnGpYx+ypboJdNrWnK17WeD7zN/xSCHnpecd5QYsBfVZde/5i+7DJ1wrf/ioSDMiEjagXmyNWAE3V2C9f1hNg==} + + are-we-there-yet@3.0.1: + resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + aria-hidden@1.2.4: + resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} + engines: {node: '>=10'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + better-sqlite3@9.6.0: + resolution: {integrity: sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + cacache@15.3.0: + resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==} + engines: {node: '>= 10'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001617: + resolution: {integrity: sha512-mLyjzNI9I+Pix8zwcrpxEbGlfqOkF9kM3ptzmKNw5tizSyYwMe+nGLTqMK9cO+0E+Bh6TsBxNAaHWEM8xwSsmA==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + class-variance-authority@0.7.0: + resolution: {integrity: sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-color@2.0.4: + resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==} + engines: {node: '>=0.10'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + clsx@2.0.0: + resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} + engines: {node: '>=6'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + date-fns@3.6.0: + resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + detect-libc@2.0.2: + resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} + engines: {node: '>=8'} + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + difflib@0.2.4: + resolution: {integrity: sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + dreamopt@0.8.0: + resolution: {integrity: sha512-vyJTp8+mC+G+5dfgsY+r3ckxlz+QMX40VjPQsZc5gxVAxLmi64TBoVkP54A/pRAXMXsbu2GMMBrZPxNv23waMg==} + engines: {node: '>=0.4.0'} + + drizzle-kit@0.21.4: + resolution: {integrity: sha512-Nxcc1ONJLRgbhmR+azxjNF9Ly9privNLEIgW53c92whb4xp8jZLH1kMCh/54ci1mTMuYxPdOukqLwJ8wRudNwA==} + hasBin: true + + drizzle-orm@0.30.10: + resolution: {integrity: sha512-IRy/QmMWw9lAQHpwbUh1b8fcn27S/a9zMIzqea1WNOxK9/4EB8gIo+FZWLiPXzl2n9ixGSv8BhsLZiOppWEwBw==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=3' + '@electric-sql/pglite': '>=0.1.1' + '@libsql/client': '*' + '@neondatabase/serverless': '>=0.1' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/react': '>=18' + '@types/sql.js': '*' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=13.2.0' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + react: '>=18' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/react': + optional: true + '@types/sql.js': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + react: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + + es6-iterator@2.0.3: + resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + + es6-symbol@3.1.4: + resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} + engines: {node: '>=0.12'} + + es6-weak-map@2.0.3: + resolution: {integrity: sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==} + + esbuild-register@3.5.0: + resolution: {integrity: sha512-+4G/XmakeBAsvJuDugJvtyF1x+XJT4FMocynNpxrvEBViirpfUn2PgNpCHedfWhF4WokNsO/OvMKrmJOIJsI5A==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} + engines: {node: '>=12'} + hasBin: true + + esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + + event-emitter@0.3.5: + resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + ext@1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + + fast-equals@5.0.1: + resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==} + engines: {node: '>=6.0.0'} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + + foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs-monkey@1.0.6: + resolution: {integrity: sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gauge@4.0.4: + resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-tsconfig@4.7.5: + resolution: {integrity: sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.3.14: + resolution: {integrity: sha512-4fkAqu93xe9Mk7le9v0y3VrPDqLKHarNi2s4Pv7f2yOvfhWfhc7hRPHC/JyqMqb8B/Dt/eGS4n7ykwf3fOsl8g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + hanji@0.0.5: + resolution: {integrity: sha512-Abxw1Lq+TnYiL4BueXqMau222fPSPMFtya8HdpWsz/xVAhifXou71mPh/kY2+08RgFcVccjG3uZHs6K5HAe3zw==} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + heap@0.2.7: + resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} + + http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + + http-proxy-agent@4.0.1: + resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} + engines: {node: '>= 6'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + ip-address@9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + + jiti@1.21.0: + resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} + hasBin: true + + js-base64@3.7.7: + resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + + json-diff@0.9.0: + resolution: {integrity: sha512-cVnggDrVkAAA3OvFfHpFEhOnmcsUpleEKq4d4O8sQWWSH40MBrWstKigVB1kGrgLWzuom+7rRdaCsnBD6VyObQ==} + hasBin: true + + libsql@0.4.6: + resolution: {integrity: sha512-F5M+ltteK6dCcpjMahrkgT96uFJvVI8aQ4r9f2AzHQjC7BkAYtvfMSTWGvRBezRgMUIU2h1Sy0pF9nOGOD5iyA==} + os: [darwin, linux, win32] + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + lilconfig@3.1.1: + resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lodash.throttle@4.1.1: + resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@10.2.2: + resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} + engines: {node: 14 || >=16.14} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lru-queue@0.1.0: + resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} + + lucia@3.2.1: + resolution: {integrity: sha512-yIVBS/wU3R+8cLClh2ksBNxqHkAd0VUcjvib53azkSdRT1cPkKuFglkxFsghuspaioX+AHhmIECEkdOz/vIJsQ==} + + lucide-react@0.378.0: + resolution: {integrity: sha512-u6EPU8juLUk9ytRcyapkWI18epAv3RU+6+TC23ivjR0e+glWKBobFeSgRwOIJihzktILQuy6E0E80P2jVTDR5g==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 + + luxon@3.5.0: + resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} + engines: {node: '>=12'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + make-fetch-happen@9.1.0: + resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} + engines: {node: '>= 10'} + + memfs-browser@3.5.10302: + resolution: {integrity: sha512-JJTc/nh3ig05O0gBBGZjTCPOyydaTxNF0uHYBrcc1gHNnO+KIHIvo0Y1FKCJsaei6FCl8C6xfQomXqu+cuzkIw==} + + memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + + memoizee@0.4.15: + resolution: {integrity: sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + mini-svg-data-uri@1.4.4: + resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} + hasBin: true + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.4: + resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass-collect@1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} + + minipass-fetch@1.4.1: + resolution: {integrity: sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==} + engines: {node: '>=8'} + + minipass-flush@1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.1: + resolution: {integrity: sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-build-utils@1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + next-themes@0.3.0: + resolution: {integrity: sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==} + peerDependencies: + react: ^16.8 || ^17 || ^18 + react-dom: ^16.8 || ^17 || ^18 + + next-tick@1.1.0: + resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + + next@14.2.3: + resolution: {integrity: sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==} + engines: {node: '>=18.17.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + sass: + optional: true + + node-abi@3.62.0: + resolution: {integrity: sha512-CPMcGa+y33xuL1E0TcNIu4YyaZCxnnvkVaEXrsosR3FxN+fV8xvb7Mzpb7IgKler10qeMkE6+Dp8qJhpzdq35g==} + engines: {node: '>=10'} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-gyp@8.4.1: + resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==} + engines: {node: '>= 10.12.0'} + hasBin: true + + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npmlog@6.0.2: + resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + oslo@1.2.0: + resolution: {integrity: sha512-OoFX6rDsNcOQVAD2gQD/z03u4vEjWZLzJtwkmgfRF+KpQUXwdgEXErD7zNhyowmHwHefP+PM9Pw13pgpHMRlzw==} + + oslo@1.2.1: + resolution: {integrity: sha512-HfIhB5ruTdQv0XX2XlncWQiJ5SIHZ7NHZhVyHth0CSZ/xzge00etRyYy/3wp/Dsu+PkxMC+6+B2lS/GcKoewkA==} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.0: + resolution: {integrity: sha512-LNHTaVkzaYaLGlO+0u3rQTz7QrHTFOuKyba9JMTQutkmtNew8dw8wOD7mTU/5fCPZzCWpfW0XnQKzY61P0aTaw==} + engines: {node: '>=16 || 14 >=14.17'} + + picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + + picocolors@1.1.0: + resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.0.1: + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-nested@6.0.1: + resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.0.16: + resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.4.47: + resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} + engines: {node: ^10 || ^12 || >=14} + + prebuild-install@7.1.2: + resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==} + engines: {node: '>=10'} + hasBin: true + + promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + + promise-limit@2.7.0: + resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==} + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + react-day-picker@8.10.1: + resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==} + peerDependencies: + date-fns: ^2.28.0 || ^3.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-hook-form@7.53.0: + resolution: {integrity: sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-if@4.1.5: + resolution: {integrity: sha512-Uk+Ub2gC83PAakuU4+7iLdTEP4LPi2ihNEPCtz/vr8SLGbzkMApbpYbkDZ5z9zYXurd0gg+EK/bpOLFFC1r1eQ==} + engines: {node: '>=12'} + peerDependencies: + react: ^16.x || ^17.x || ^18.x + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-remove-scroll-bar@2.3.6: + resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.6.0: + resolution: {integrity: sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-smooth@4.0.1: + resolution: {integrity: sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + + react-style-singleton@2.2.1: + resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-timer-hook@3.0.7: + resolution: {integrity: sha512-ATpNcU+PQRxxfNBPVqce2+REtjGAlwmfoNQfcEBMZFxPj0r3GYdKhyPHdStvqrejejEi0QvqaJZjy2lBlFvAsA==} + peerDependencies: + react: '>=16.8.0' + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react-transition-state@2.1.2: + resolution: {integrity: sha512-RkDYBkj1V1ZqBA5AwQPrMt2Uagwsx6b//GVJdRDhs/t0o66w2nhQiyHyFGQEI60mgtbaIdLm8yhBRCvhA+FxEg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + + recharts@2.13.3: + resolution: {integrity: sha512-YDZ9dOfK9t3ycwxgKbrnDlRC4BHdjlY73fet3a0C1+qGMjXVZe6+VXmpOIIhzkje5MMEL8AN4hLIe4AMskBzlA==} + engines: {node: '>=14'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + regression@2.0.1: + resolution: {integrity: sha512-A4XYsc37dsBaNOgEjkJKzfJlE394IMmUPlI/p3TTI9u3T+2a+eox5Pr/CPUqF0eszeWZJPAc6QkroAhuUpWDJQ==} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@7.6.2: + resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} + engines: {node: '>=10'} + hasBin: true + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@6.2.1: + resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==} + engines: {node: '>= 10'} + + socks@2.8.3: + resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + sonner@1.5.0: + resolution: {integrity: sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + + source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + sqlite3@5.1.7: + resolution: {integrity: sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==} + + sqlite@5.1.1: + resolution: {integrity: sha512-oBkezXa2hnkfuJwUo44Hl9hS3er+YFtueifoajrgidvqsJRQFpc5fKoAkAor1O5ZnLoa28GBScfHXs8j0K358Q==} + + ssri@8.0.1: + resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} + engines: {node: '>= 8'} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + styled-jsx@5.1.1: + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + swr@2.2.5: + resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + + tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + + tailwind-merge@2.5.3: + resolution: {integrity: sha512-d9ZolCAIzom1nf/5p4LdD5zvjmgSxY0BGgdSvmXIoMYAiPdAW/dSpP7joCDYFY7r/HkEa2qmPtkgsu0xjQeQtw==} + + tailwindcss-animate@1.0.7: + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + + tailwindcss@3.4.13: + resolution: {integrity: sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==} + engines: {node: '>=14.0.0'} + hasBin: true + + tar-fs@2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + timers-ext@0.1.7: + resolution: {integrity: sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + type@2.7.2: + resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==} + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + unique-filename@1.1.1: + resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} + + unique-slug@2.0.2: + resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} + + use-callback-ref@1.3.2: + resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-debounce@10.0.3: + resolution: {integrity: sha512-DxQSI9ZKso689WM1mjgGU3ozcxU1TJElBJ3X6S4SMzMNcm2lVH0AHmyXB+K7ewjz2BSUKJTDqTcwtSMRfB89dg==} + engines: {node: '>= 16.0.0'} + peerDependencies: + react: '*' + + use-sidecar@1.1.2: + resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.2.2: + resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@11.0.2: + resolution: {integrity: sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==} + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + victory-vendor@36.9.2: + resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yaml@2.4.2: + resolution: {integrity: sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==} + engines: {node: '>= 14'} + hasBin: true + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@babel/runtime@7.24.5': + dependencies: + regenerator-runtime: 0.14.1 + + '@biomejs/biome@1.9.3': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 1.9.3 + '@biomejs/cli-darwin-x64': 1.9.3 + '@biomejs/cli-linux-arm64': 1.9.3 + '@biomejs/cli-linux-arm64-musl': 1.9.3 + '@biomejs/cli-linux-x64': 1.9.3 + '@biomejs/cli-linux-x64-musl': 1.9.3 + '@biomejs/cli-win32-arm64': 1.9.3 + '@biomejs/cli-win32-x64': 1.9.3 + + '@biomejs/cli-darwin-arm64@1.9.3': + optional: true + + '@biomejs/cli-darwin-x64@1.9.3': + optional: true + + '@biomejs/cli-linux-arm64-musl@1.9.3': + optional: true + + '@biomejs/cli-linux-arm64@1.9.3': + optional: true + + '@biomejs/cli-linux-x64-musl@1.9.3': + optional: true + + '@biomejs/cli-linux-x64@1.9.3': + optional: true + + '@biomejs/cli-win32-arm64@1.9.3': + optional: true + + '@biomejs/cli-win32-x64@1.9.3': + optional: true + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/core@0.45.0': + dependencies: + tslib: 2.6.2 + optional: true + + '@emnapi/runtime@0.45.0': + dependencies: + tslib: 2.6.2 + optional: true + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.7.5 + + '@esbuild/aix-ppc64@0.19.12': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.19.12': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.19.12': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.19.12': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.19.12': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.19.12': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.19.12': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.19.12': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.19.12': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.19.12': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.19.12': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.19.12': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.19.12': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.19.12': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.19.12': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.19.12': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.19.12': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.19.12': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.19.12': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.19.12': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.19.12': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.19.12': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.19.12': + optional: true + + '@faker-js/faker@9.2.0': {} + + '@floating-ui/core@1.6.1': + dependencies: + '@floating-ui/utils': 0.2.8 + + '@floating-ui/dom@1.6.5': + dependencies: + '@floating-ui/core': 1.6.1 + '@floating-ui/utils': 0.2.8 + + '@floating-ui/react-dom@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.6.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/react-dom@2.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.6.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/react-dom@2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.6.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/react@0.19.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tabbable: 6.2.0 + + '@floating-ui/react@0.26.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@floating-ui/utils': 0.2.8 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tabbable: 6.2.0 + + '@floating-ui/utils@0.2.8': {} + + '@gar/promisify@1.1.3': + optional: true + + '@headlessui/react@1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/react-virtual': 3.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + client-only: 0.0.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@headlessui/react@2.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react': 0.26.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/focus': 3.18.3(react@18.3.1) + '@react-aria/interactions': 3.22.3(react@18.3.1) + '@tanstack/react-virtual': 3.10.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@headlessui/tailwindcss@0.2.1(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3)))': + dependencies: + tailwindcss: 3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3)) + + '@hookform/resolvers@3.9.0(react-hook-form@7.53.0(react@18.3.1))': + dependencies: + react-hook-form: 7.53.0(react@18.3.1) + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.4.15': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + + '@libsql/client@0.14.0': + dependencies: + '@libsql/core': 0.14.0 + '@libsql/hrana-client': 0.7.0 + js-base64: 3.7.7 + libsql: 0.4.6 + promise-limit: 2.7.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/core@0.14.0': + dependencies: + js-base64: 3.7.7 + + '@libsql/darwin-arm64@0.4.6': + optional: true + + '@libsql/darwin-x64@0.4.6': + optional: true + + '@libsql/hrana-client@0.7.0': + dependencies: + '@libsql/isomorphic-fetch': 0.3.1 + '@libsql/isomorphic-ws': 0.1.5 + js-base64: 3.7.7 + node-fetch: 3.3.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/isomorphic-fetch@0.3.1': {} + + '@libsql/isomorphic-ws@0.1.5': + dependencies: + '@types/ws': 8.5.12 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/linux-arm64-gnu@0.4.6': + optional: true + + '@libsql/linux-arm64-musl@0.4.6': + optional: true + + '@libsql/linux-x64-gnu@0.4.6': + optional: true + + '@libsql/linux-x64-musl@0.4.6': + optional: true + + '@libsql/win32-x64-msvc@0.4.6': + optional: true + + '@lucia-auth/adapter-drizzle@1.1.0(drizzle-orm@0.30.10(@libsql/client@0.14.0)(@types/better-sqlite3@7.6.11)(@types/react@18.3.11)(better-sqlite3@9.6.0)(react@18.3.1)(sqlite3@5.1.7))(lucia@3.2.1)': + dependencies: + drizzle-orm: 0.30.10(@libsql/client@0.14.0)(@types/better-sqlite3@7.6.11)(@types/react@18.3.11)(better-sqlite3@9.6.0)(react@18.3.1)(sqlite3@5.1.7) + lucia: 3.2.1 + + '@neon-rs/load@0.0.4': {} + + '@next/env@14.2.3': {} + + '@next/swc-darwin-arm64@14.2.3': + optional: true + + '@next/swc-darwin-x64@14.2.3': + optional: true + + '@next/swc-linux-arm64-gnu@14.2.3': + optional: true + + '@next/swc-linux-arm64-musl@14.2.3': + optional: true + + '@next/swc-linux-x64-gnu@14.2.3': + optional: true + + '@next/swc-linux-x64-musl@14.2.3': + optional: true + + '@next/swc-win32-arm64-msvc@14.2.3': + optional: true + + '@next/swc-win32-ia32-msvc@14.2.3': + optional: true + + '@next/swc-win32-x64-msvc@14.2.3': + optional: true + + '@node-rs/argon2-android-arm-eabi@1.7.0': + optional: true + + '@node-rs/argon2-android-arm64@1.7.0': + optional: true + + '@node-rs/argon2-darwin-arm64@1.7.0': + optional: true + + '@node-rs/argon2-darwin-x64@1.7.0': + optional: true + + '@node-rs/argon2-freebsd-x64@1.7.0': + optional: true + + '@node-rs/argon2-linux-arm-gnueabihf@1.7.0': + optional: true + + '@node-rs/argon2-linux-arm64-gnu@1.7.0': + optional: true + + '@node-rs/argon2-linux-arm64-musl@1.7.0': + optional: true + + '@node-rs/argon2-linux-x64-gnu@1.7.0': + optional: true + + '@node-rs/argon2-linux-x64-musl@1.7.0': + optional: true + + '@node-rs/argon2-wasm32-wasi@1.7.0': + dependencies: + '@emnapi/core': 0.45.0 + '@emnapi/runtime': 0.45.0 + '@tybys/wasm-util': 0.8.3 + memfs-browser: 3.5.10302 + optional: true + + '@node-rs/argon2-win32-arm64-msvc@1.7.0': + optional: true + + '@node-rs/argon2-win32-ia32-msvc@1.7.0': + optional: true + + '@node-rs/argon2-win32-x64-msvc@1.7.0': + optional: true + + '@node-rs/argon2@1.7.0': + optionalDependencies: + '@node-rs/argon2-android-arm-eabi': 1.7.0 + '@node-rs/argon2-android-arm64': 1.7.0 + '@node-rs/argon2-darwin-arm64': 1.7.0 + '@node-rs/argon2-darwin-x64': 1.7.0 + '@node-rs/argon2-freebsd-x64': 1.7.0 + '@node-rs/argon2-linux-arm-gnueabihf': 1.7.0 + '@node-rs/argon2-linux-arm64-gnu': 1.7.0 + '@node-rs/argon2-linux-arm64-musl': 1.7.0 + '@node-rs/argon2-linux-x64-gnu': 1.7.0 + '@node-rs/argon2-linux-x64-musl': 1.7.0 + '@node-rs/argon2-wasm32-wasi': 1.7.0 + '@node-rs/argon2-win32-arm64-msvc': 1.7.0 + '@node-rs/argon2-win32-ia32-msvc': 1.7.0 + '@node-rs/argon2-win32-x64-msvc': 1.7.0 + + '@node-rs/bcrypt-android-arm-eabi@1.9.0': + optional: true + + '@node-rs/bcrypt-android-arm64@1.9.0': + optional: true + + '@node-rs/bcrypt-darwin-arm64@1.9.0': + optional: true + + '@node-rs/bcrypt-darwin-x64@1.9.0': + optional: true + + '@node-rs/bcrypt-freebsd-x64@1.9.0': + optional: true + + '@node-rs/bcrypt-linux-arm-gnueabihf@1.9.0': + optional: true + + '@node-rs/bcrypt-linux-arm64-gnu@1.9.0': + optional: true + + '@node-rs/bcrypt-linux-arm64-musl@1.9.0': + optional: true + + '@node-rs/bcrypt-linux-x64-gnu@1.9.0': + optional: true + + '@node-rs/bcrypt-linux-x64-musl@1.9.0': + optional: true + + '@node-rs/bcrypt-wasm32-wasi@1.9.0': + dependencies: + '@emnapi/core': 0.45.0 + '@emnapi/runtime': 0.45.0 + '@tybys/wasm-util': 0.8.3 + memfs-browser: 3.5.10302 + optional: true + + '@node-rs/bcrypt-win32-arm64-msvc@1.9.0': + optional: true + + '@node-rs/bcrypt-win32-ia32-msvc@1.9.0': + optional: true + + '@node-rs/bcrypt-win32-x64-msvc@1.9.0': + optional: true + + '@node-rs/bcrypt@1.9.0': + optionalDependencies: + '@node-rs/bcrypt-android-arm-eabi': 1.9.0 + '@node-rs/bcrypt-android-arm64': 1.9.0 + '@node-rs/bcrypt-darwin-arm64': 1.9.0 + '@node-rs/bcrypt-darwin-x64': 1.9.0 + '@node-rs/bcrypt-freebsd-x64': 1.9.0 + '@node-rs/bcrypt-linux-arm-gnueabihf': 1.9.0 + '@node-rs/bcrypt-linux-arm64-gnu': 1.9.0 + '@node-rs/bcrypt-linux-arm64-musl': 1.9.0 + '@node-rs/bcrypt-linux-x64-gnu': 1.9.0 + '@node-rs/bcrypt-linux-x64-musl': 1.9.0 + '@node-rs/bcrypt-wasm32-wasi': 1.9.0 + '@node-rs/bcrypt-win32-arm64-msvc': 1.9.0 + '@node-rs/bcrypt-win32-ia32-msvc': 1.9.0 + '@node-rs/bcrypt-win32-x64-msvc': 1.9.0 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@npmcli/fs@1.1.1': + dependencies: + '@gar/promisify': 1.1.3 + semver: 7.6.2 + optional: true + + '@npmcli/move-file@1.1.2': + dependencies: + mkdirp: 1.0.4 + rimraf: 3.0.2 + optional: true + + '@oslojs/asn1@1.0.0': + dependencies: + '@oslojs/binary': 1.0.0 + + '@oslojs/binary@1.0.0': {} + + '@oslojs/crypto@1.0.1': + dependencies: + '@oslojs/asn1': 1.0.0 + '@oslojs/binary': 1.0.0 + + '@oslojs/encoding@1.1.0': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@radix-ui/number@1.1.0': {} + + '@radix-ui/primitive@1.1.0': {} + + '@radix-ui/react-alert-dialog@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-arrow@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-avatar@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-context': 1.1.1(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.11)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.11 + + '@radix-ui/react-context@1.1.0(@types/react@18.3.11)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.11 + + '@radix-ui/react-context@1.1.1(@types/react@18.3.11)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.11 + + '@radix-ui/react-dialog@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.11)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.6.0(@types/react@18.3.11)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-direction@1.1.0(@types/react@18.3.11)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.11 + + '@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-dropdown-menu@2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-menu': 2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-focus-guards@1.1.1(@types/react@18.3.11)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.11 + + '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-hover-card@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-icons@1.3.0(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@radix-ui/react-id@1.1.0(@types/react@18.3.11)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.11 + + '@radix-ui/react-label@2.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-menu@2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.6.0(@types/react@18.3.11)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-popper@1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-portal@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-presence@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-scroll-area@1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-select@2.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.6.0(@types/react@18.3.11)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-slot@1.1.0(@types/react@18.3.11)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.11 + + '@radix-ui/react-tabs@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-context': 1.1.1(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-toast@1.2.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.11)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.11 + + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.11)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.11 + + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.11)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.11 + + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.11)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.11 + + '@radix-ui/react-use-previous@1.1.0(@types/react@18.3.11)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.11 + + '@radix-ui/react-use-rect@1.1.0(@types/react@18.3.11)(react@18.3.1)': + dependencies: + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.11 + + '@radix-ui/react-use-size@1.1.0(@types/react@18.3.11)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.11 + + '@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.1 + + '@radix-ui/rect@1.1.0': {} + + '@react-aria/focus@3.18.3(react@18.3.1)': + dependencies: + '@react-aria/interactions': 3.22.3(react@18.3.1) + '@react-aria/utils': 3.25.3(react@18.3.1) + '@react-types/shared': 3.25.0(react@18.3.1) + '@swc/helpers': 0.5.5 + clsx: 2.1.1 + react: 18.3.1 + + '@react-aria/interactions@3.22.3(react@18.3.1)': + dependencies: + '@react-aria/ssr': 3.9.6(react@18.3.1) + '@react-aria/utils': 3.25.3(react@18.3.1) + '@react-types/shared': 3.25.0(react@18.3.1) + '@swc/helpers': 0.5.5 + react: 18.3.1 + + '@react-aria/ssr@3.9.6(react@18.3.1)': + dependencies: + '@swc/helpers': 0.5.5 + react: 18.3.1 + + '@react-aria/utils@3.25.3(react@18.3.1)': + dependencies: + '@react-aria/ssr': 3.9.6(react@18.3.1) + '@react-stately/utils': 3.10.4(react@18.3.1) + '@react-types/shared': 3.25.0(react@18.3.1) + '@swc/helpers': 0.5.5 + clsx: 2.1.1 + react: 18.3.1 + + '@react-stately/utils@3.10.4(react@18.3.1)': + dependencies: + '@swc/helpers': 0.5.5 + react: 18.3.1 + + '@react-types/shared@3.25.0(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@remixicon/react@4.3.0(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.5': + dependencies: + '@swc/counter': 0.1.3 + tslib: 2.6.2 + + '@tailwindcss/forms@0.5.9(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3)))': + dependencies: + mini-svg-data-uri: 1.4.4 + tailwindcss: 3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3)) + + '@tanstack/react-table@8.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/table-core': 8.20.5 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@tanstack/react-virtual@3.10.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/virtual-core': 3.10.8 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@tanstack/react-virtual@3.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/virtual-core': 3.5.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@tanstack/table-core@8.20.5': {} + + '@tanstack/virtual-core@3.10.8': {} + + '@tanstack/virtual-core@3.5.0': {} + + '@tootallnate/once@1.1.2': + optional: true + + '@tremor/react@3.18.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3)))': + dependencies: + '@floating-ui/react': 0.19.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@headlessui/react': 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@headlessui/tailwindcss': 0.2.1(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3))) + date-fns: 3.6.0 + react: 18.3.1 + react-day-picker: 8.10.1(date-fns@3.6.0)(react@18.3.1) + react-dom: 18.3.1(react@18.3.1) + react-transition-state: 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + recharts: 2.13.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tailwind-merge: 2.5.3 + transitivePeerDependencies: + - tailwindcss + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@tybys/wasm-util@0.8.3': + dependencies: + tslib: 2.6.2 + optional: true + + '@types/better-sqlite3@7.6.11': + dependencies: + '@types/node': 20.16.11 + optional: true + + '@types/d3-array@3.2.1': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.0': {} + + '@types/d3-scale@4.0.8': + dependencies: + '@types/d3-time': 3.0.3 + + '@types/d3-shape@3.1.6': + dependencies: + '@types/d3-path': 3.1.0 + + '@types/d3-time@3.0.3': {} + + '@types/d3-timer@3.0.2': {} + + '@types/lodash@4.17.13': {} + + '@types/luxon@3.4.2': {} + + '@types/node@20.16.11': + dependencies: + undici-types: 6.19.8 + + '@types/prop-types@15.7.12': {} + + '@types/react-dom@18.3.1': + dependencies: + '@types/react': 18.3.11 + + '@types/react@18.3.11': + dependencies: + '@types/prop-types': 15.7.12 + csstype: 3.1.3 + + '@types/ws@8.5.12': + dependencies: + '@types/node': 20.16.11 + + abbrev@1.1.1: + optional: true + + acorn-walk@8.3.4: + dependencies: + acorn: 8.14.0 + + acorn@8.14.0: {} + + agent-base@6.0.2: + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + optional: true + + agentkeepalive@4.5.0: + dependencies: + humanize-ms: 1.2.1 + optional: true + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + optional: true + + ansi-regex@5.0.1: {} + + ansi-regex@6.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + aproba@2.0.0: + optional: true + + arctic@1.9.2: + dependencies: + oslo: 1.2.0 + + are-we-there-yet@3.0.1: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + optional: true + + arg@4.1.3: {} + + arg@5.0.2: {} + + aria-hidden@1.2.4: + dependencies: + tslib: 2.6.2 + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + better-sqlite3@9.6.0: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.2 + optional: true + + binary-extensions@2.3.0: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + optional: true + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.2: + dependencies: + fill-range: 7.0.1 + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + cacache@15.3.0: + dependencies: + '@npmcli/fs': 1.1.1 + '@npmcli/move-file': 1.1.2 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 7.2.3 + infer-owner: 1.0.4 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1 + rimraf: 3.0.2 + ssri: 8.0.1 + tar: 6.2.1 + unique-filename: 1.1.1 + transitivePeerDependencies: + - bluebird + optional: true + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001617: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chownr@1.1.4: {} + + chownr@2.0.0: {} + + class-variance-authority@0.7.0: + dependencies: + clsx: 2.0.0 + + clean-stack@2.2.0: + optional: true + + cli-color@2.0.4: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-iterator: 2.0.3 + memoizee: 0.4.15 + timers-ext: 0.1.7 + + client-only@0.0.1: {} + + clsx@2.0.0: {} + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-support@1.1.3: + optional: true + + commander@4.1.1: {} + + commander@9.5.0: {} + + concat-map@0.0.1: + optional: true + + console-control-strings@1.1.0: + optional: true + + create-require@1.1.1: {} + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.1.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.0: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d@1.0.2: + dependencies: + es5-ext: 0.10.64 + type: 2.7.2 + + data-uri-to-buffer@4.0.1: {} + + date-fns@3.6.0: {} + + date-fns@4.1.0: {} + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + decimal.js-light@2.5.1: {} + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + delegates@1.0.0: + optional: true + + detect-libc@2.0.2: {} + + detect-libc@2.0.3: {} + + detect-node-es@1.1.0: {} + + didyoumean@1.2.2: {} + + diff@4.0.2: {} + + difflib@0.2.4: + dependencies: + heap: 0.2.7 + + dlv@1.1.3: {} + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.24.5 + csstype: 3.1.3 + + dreamopt@0.8.0: + dependencies: + wordwrap: 1.0.0 + + drizzle-kit@0.21.4: + dependencies: + '@esbuild-kit/esm-loader': 2.6.5 + commander: 9.5.0 + env-paths: 3.0.0 + esbuild: 0.19.12 + esbuild-register: 3.5.0(esbuild@0.19.12) + glob: 8.1.0 + hanji: 0.0.5 + json-diff: 0.9.0 + zod: 3.23.8 + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.30.10(@libsql/client@0.14.0)(@types/better-sqlite3@7.6.11)(@types/react@18.3.11)(better-sqlite3@9.6.0)(react@18.3.1)(sqlite3@5.1.7): + optionalDependencies: + '@libsql/client': 0.14.0 + '@types/better-sqlite3': 7.6.11 + '@types/react': 18.3.11 + better-sqlite3: 9.6.0 + react: 18.3.1 + sqlite3: 5.1.7 + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + + env-paths@2.2.1: + optional: true + + env-paths@3.0.0: {} + + err-code@2.0.3: + optional: true + + es5-ext@0.10.64: + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + esniff: 2.0.1 + next-tick: 1.1.0 + + es6-iterator@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-symbol: 3.1.4 + + es6-symbol@3.1.4: + dependencies: + d: 1.0.2 + ext: 1.7.0 + + es6-weak-map@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + + esbuild-register@3.5.0(esbuild@0.19.12): + dependencies: + debug: 4.3.4 + esbuild: 0.19.12 + transitivePeerDependencies: + - supports-color + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.19.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 + + esniff@2.0.1: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + type: 2.7.2 + + event-emitter@0.3.5: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + + eventemitter3@4.0.7: {} + + expand-template@2.0.3: {} + + ext@1.7.0: + dependencies: + type: 2.7.2 + + fast-equals@5.0.1: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + file-uri-to-path@1.0.0: {} + + fill-range@7.0.1: + dependencies: + to-regex-range: 5.0.1 + + foreground-child@3.1.1: + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + fs-constants@1.0.0: {} + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs-monkey@1.0.6: + optional: true + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gauge@4.0.4: + dependencies: + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + optional: true + + get-nonce@1.0.1: {} + + get-tsconfig@4.7.5: + dependencies: + resolve-pkg-maps: 1.0.0 + + github-from-package@0.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.3.14: + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.4 + minipass: 7.1.1 + path-scurry: 1.11.0 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + optional: true + + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + + graceful-fs@4.2.11: {} + + hanji@0.0.5: + dependencies: + lodash.throttle: 4.1.1 + sisteransi: 1.0.5 + + has-unicode@2.0.1: + optional: true + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + heap@0.2.7: {} + + http-cache-semantics@4.1.1: + optional: true + + http-proxy-agent@4.0.1: + dependencies: + '@tootallnate/once': 1.1.2 + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + optional: true + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + optional: true + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.2 + optional: true + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + optional: true + + ieee754@1.2.1: {} + + imurmurhash@0.1.4: + optional: true + + indent-string@4.0.0: + optional: true + + infer-owner@1.0.4: + optional: true + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + internmap@2.0.3: {} + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + ip-address@9.0.5: + dependencies: + jsbn: 1.1.0 + sprintf-js: 1.1.3 + optional: true + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.13.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-lambda@1.0.1: + optional: true + + is-number@7.0.0: {} + + is-promise@2.2.2: {} + + isexe@2.0.0: {} + + jackspeak@2.3.6: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@1.21.0: {} + + js-base64@3.7.7: {} + + js-tokens@4.0.0: {} + + jsbn@1.1.0: + optional: true + + json-diff@0.9.0: + dependencies: + cli-color: 2.0.4 + difflib: 0.2.4 + dreamopt: 0.8.0 + + libsql@0.4.6: + dependencies: + '@neon-rs/load': 0.0.4 + detect-libc: 2.0.2 + optionalDependencies: + '@libsql/darwin-arm64': 0.4.6 + '@libsql/darwin-x64': 0.4.6 + '@libsql/linux-arm64-gnu': 0.4.6 + '@libsql/linux-arm64-musl': 0.4.6 + '@libsql/linux-x64-gnu': 0.4.6 + '@libsql/linux-x64-musl': 0.4.6 + '@libsql/win32-x64-msvc': 0.4.6 + + lilconfig@2.1.0: {} + + lilconfig@3.1.1: {} + + lines-and-columns@1.2.4: {} + + lodash.throttle@4.1.1: {} + + lodash@4.17.21: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@10.2.2: {} + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + optional: true + + lru-queue@0.1.0: + dependencies: + es5-ext: 0.10.64 + + lucia@3.2.1: + dependencies: + '@oslojs/crypto': 1.0.1 + '@oslojs/encoding': 1.1.0 + + lucide-react@0.378.0(react@18.3.1): + dependencies: + react: 18.3.1 + + luxon@3.5.0: {} + + make-error@1.3.6: {} + + make-fetch-happen@9.1.0: + dependencies: + agentkeepalive: 4.5.0 + cacache: 15.3.0 + http-cache-semantics: 4.1.1 + http-proxy-agent: 4.0.1 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-fetch: 1.4.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + promise-retry: 2.0.1 + socks-proxy-agent: 6.2.1 + ssri: 8.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + optional: true + + memfs-browser@3.5.10302: + dependencies: + memfs: 3.5.3 + optional: true + + memfs@3.5.3: + dependencies: + fs-monkey: 1.0.6 + optional: true + + memoizee@0.4.15: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-weak-map: 2.0.3 + event-emitter: 0.3.5 + is-promise: 2.2.2 + lru-queue: 0.1.0 + next-tick: 1.1.0 + timers-ext: 0.1.7 + + merge2@1.4.1: {} + + micromatch@4.0.5: + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + + mimic-response@3.1.0: {} + + mini-svg-data-uri@1.4.4: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + optional: true + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.4: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + minipass-collect@1.0.2: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-fetch@1.4.1: + dependencies: + minipass: 3.3.6 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + optional: true + + minipass-flush@1.0.5: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-sized@1.0.3: + dependencies: + minipass: 3.3.6 + optional: true + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minipass@7.1.1: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mkdirp-classic@0.5.3: {} + + mkdirp@1.0.4: {} + + ms@2.1.2: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.7: {} + + napi-build-utils@1.0.2: {} + + negotiator@0.6.4: + optional: true + + next-themes@0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + next-tick@1.1.0: {} + + next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@next/env': 14.2.3 + '@swc/helpers': 0.5.5 + busboy: 1.6.0 + caniuse-lite: 1.0.30001617 + graceful-fs: 4.2.11 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.1(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 14.2.3 + '@next/swc-darwin-x64': 14.2.3 + '@next/swc-linux-arm64-gnu': 14.2.3 + '@next/swc-linux-arm64-musl': 14.2.3 + '@next/swc-linux-x64-gnu': 14.2.3 + '@next/swc-linux-x64-musl': 14.2.3 + '@next/swc-win32-arm64-msvc': 14.2.3 + '@next/swc-win32-ia32-msvc': 14.2.3 + '@next/swc-win32-x64-msvc': 14.2.3 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-abi@3.62.0: + dependencies: + semver: 7.6.2 + + node-addon-api@7.1.1: {} + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-gyp@8.4.1: + dependencies: + env-paths: 2.2.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + make-fetch-happen: 9.1.0 + nopt: 5.0.0 + npmlog: 6.0.2 + rimraf: 3.0.2 + semver: 7.6.2 + tar: 6.2.1 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - supports-color + optional: true + + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + optional: true + + normalize-path@3.0.0: {} + + npmlog@6.0.2: + dependencies: + are-we-there-yet: 3.0.1 + console-control-strings: 1.1.0 + gauge: 4.0.4 + set-blocking: 2.0.0 + optional: true + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + oslo@1.2.0: + dependencies: + '@node-rs/argon2': 1.7.0 + '@node-rs/bcrypt': 1.9.0 + + oslo@1.2.1: + dependencies: + '@node-rs/argon2': 1.7.0 + '@node-rs/bcrypt': 1.9.0 + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + optional: true + + path-is-absolute@1.0.1: + optional: true + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.0: + dependencies: + lru-cache: 10.2.2 + minipass: 7.1.1 + + picocolors@1.0.0: {} + + picocolors@1.1.0: {} + + picomatch@2.3.1: {} + + pify@2.3.0: {} + + pirates@4.0.6: {} + + postcss-import@15.1.0(postcss@8.4.47): + dependencies: + postcss: 8.4.47 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.8 + + postcss-js@4.0.1(postcss@8.4.47): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.47 + + postcss-load-config@4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3)): + dependencies: + lilconfig: 3.1.1 + yaml: 2.4.2 + optionalDependencies: + postcss: 8.4.47 + ts-node: 10.9.2(@types/node@20.16.11)(typescript@5.6.3) + + postcss-nested@6.0.1(postcss@8.4.47): + dependencies: + postcss: 8.4.47 + postcss-selector-parser: 6.0.16 + + postcss-selector-parser@6.0.16: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.4.31: + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.2.0 + + postcss@8.4.47: + dependencies: + nanoid: 3.3.7 + picocolors: 1.1.0 + source-map-js: 1.2.1 + + prebuild-install@7.1.2: + dependencies: + detect-libc: 2.0.3 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.62.0 + pump: 3.0.0 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + + promise-inflight@1.0.1: + optional: true + + promise-limit@2.7.0: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + optional: true + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + pump@3.0.0: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + + queue-microtask@1.2.3: {} + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + react-day-picker@8.10.1(date-fns@3.6.0)(react@18.3.1): + dependencies: + date-fns: 3.6.0 + react: 18.3.1 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-hook-form@7.53.0(react@18.3.1): + dependencies: + react: 18.3.1 + + react-if@4.1.5(react@18.3.1): + dependencies: + react: 18.3.1 + + react-is@16.13.1: {} + + react-is@18.3.1: {} + + react-remove-scroll-bar@2.3.6(@types/react@18.3.11)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.1(@types/react@18.3.11)(react@18.3.1) + tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.3.11 + + react-remove-scroll@2.6.0(@types/react@18.3.11)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.6(@types/react@18.3.11)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.11)(react@18.3.1) + tslib: 2.6.2 + use-callback-ref: 1.3.2(@types/react@18.3.11)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.11)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + + react-smooth@4.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + fast-equals: 5.0.1 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + react-style-singleton@2.2.1(@types/react@18.3.11)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + invariant: 2.2.4 + react: 18.3.1 + tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.3.11 + + react-timer-hook@3.0.7(react@18.3.1): + dependencies: + react: 18.3.1 + + react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.24.5 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-transition-state@2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + recharts-scale@0.4.5: + dependencies: + decimal.js-light: 2.5.1 + + recharts@2.13.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + clsx: 2.1.1 + eventemitter3: 4.0.7 + lodash: 4.17.21 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 18.3.1 + react-smooth: 4.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + recharts-scale: 0.4.5 + tiny-invariant: 1.3.3 + victory-vendor: 36.9.2 + + regenerator-runtime@0.14.1: {} + + regression@2.0.1: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.8: + dependencies: + is-core-module: 2.13.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + retry@0.12.0: + optional: true + + reusify@1.0.4: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + optional: true + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: + optional: true + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@7.6.2: {} + + set-blocking@2.0.0: + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@3.0.7: + optional: true + + signal-exit@4.1.0: {} + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + sisteransi@1.0.5: {} + + smart-buffer@4.2.0: + optional: true + + socks-proxy-agent@6.2.1: + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + socks: 2.8.3 + transitivePeerDependencies: + - supports-color + optional: true + + socks@2.8.3: + dependencies: + ip-address: 9.0.5 + smart-buffer: 4.2.0 + optional: true + + sonner@1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + source-map-js@1.2.0: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + sprintf-js@1.1.3: + optional: true + + sqlite3@5.1.7: + dependencies: + bindings: 1.5.0 + node-addon-api: 7.1.1 + prebuild-install: 7.1.2 + tar: 6.2.1 + optionalDependencies: + node-gyp: 8.4.1 + transitivePeerDependencies: + - bluebird + - supports-color + + sqlite@5.1.1: {} + + ssri@8.0.1: + dependencies: + minipass: 3.3.6 + optional: true + + streamsearch@1.1.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.0.1 + + strip-json-comments@2.0.1: {} + + styled-jsx@5.1.1(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + commander: 4.1.1 + glob: 10.3.14 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + + supports-preserve-symlinks-flag@1.0.0: {} + + swr@2.2.5(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + use-sync-external-store: 1.2.2(react@18.3.1) + + tabbable@6.2.0: {} + + tailwind-merge@2.5.3: {} + + tailwindcss-animate@1.0.7(tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3))): + dependencies: + tailwindcss: 3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3)) + + tailwindcss@3.4.13(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3)): + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.2 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.0 + lilconfig: 2.1.0 + micromatch: 4.0.5 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.47 + postcss-import: 15.1.0(postcss@8.4.47) + postcss-js: 4.0.1(postcss@8.4.47) + postcss-load-config: 4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3)) + postcss-nested: 6.0.1(postcss@8.4.47) + postcss-selector-parser: 6.0.16 + resolve: 1.22.8 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + + tar-fs@2.1.1: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + timers-ext@0.1.7: + dependencies: + es5-ext: 0.10.64 + next-tick: 1.1.0 + + tiny-invariant@1.3.3: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + ts-interface-checker@0.1.13: {} + + ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.16.11 + acorn: 8.14.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.6.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tslib@2.6.2: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + type@2.7.2: {} + + typescript@5.6.3: {} + + undici-types@6.19.8: {} + + unique-filename@1.1.1: + dependencies: + unique-slug: 2.0.2 + optional: true + + unique-slug@2.0.2: + dependencies: + imurmurhash: 0.1.4 + optional: true + + use-callback-ref@1.3.2(@types/react@18.3.11)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.3.11 + + use-debounce@10.0.3(react@18.3.1): + dependencies: + react: 18.3.1 + + use-sidecar@1.1.2(@types/react@18.3.11)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.3.11 + + use-sync-external-store@1.2.2(react@18.3.1): + dependencies: + react: 18.3.1 + + util-deprecate@1.0.2: {} + + uuid@11.0.2: {} + + v8-compile-cache-lib@3.0.1: {} + + victory-vendor@36.9.2: + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.8 + '@types/d3-shape': 3.1.6 + '@types/d3-time': 3.0.3 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + web-streams-polyfill@3.3.3: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + optional: true + + wordwrap@1.0.0: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrappy@1.0.2: {} + + ws@8.18.0: {} + + yallist@4.0.0: {} + + yaml@2.4.2: {} + + yn@3.1.1: {} + + zod@3.23.8: {} diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs new file mode 100644 index 00000000..1a69fd2a --- /dev/null +++ b/frontend/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/frontend/public/next.svg b/frontend/public/next.svg new file mode 100644 index 00000000..5174b28c --- /dev/null +++ b/frontend/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/vercel.svg b/frontend/public/vercel.svg new file mode 100644 index 00000000..d2f84222 --- /dev/null +++ b/frontend/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/repomix-output.txt b/frontend/repomix-output.txt new file mode 100644 index 00000000..68f0be14 --- /dev/null +++ b/frontend/repomix-output.txt @@ -0,0 +1,9279 @@ +This file is a merged representation of the entire codebase, combining all repository files into a single document. +Generated by Repomix on: 2024-12-09T06:29:51.427Z + +================================================================ +File Summary +================================================================ + +Purpose: +-------- +This file contains a packed representation of the entire repository's contents. +It is designed to be easily consumable by AI systems for analysis, code review, +or other automated processes. + +File Format: +------------ +The content is organized as follows: +1. This summary section +2. Repository information +3. Repository structure +4. Multiple file entries, each consisting of: + a. A separator line (================) + b. The file path (File: path/to/file) + c. Another separator line + d. The full contents of the file + e. A blank line + +Usage Guidelines: +----------------- +- This file should be treated as read-only. Any changes should be made to the + original repository files, not this packed version. +- When processing this file, use the file path to distinguish + between different files in the repository. +- Be aware that this file may contain sensitive information. Handle it with + the same level of security as you would the original repository. + +Notes: +------ +- Some files may have been excluded based on .gitignore rules and Repomix's + configuration. +- Binary files are not included in this packed representation. Please refer to + the Repository Structure section for a complete list of file paths, including + binary files. + +Additional Info: +---------------- + +For more information about Repomix, visit: https://github.com/yamadashy/repomix + +================================================================ +Repository Structure +================================================================ +.dockerignore +.env.example +.gitignore +biome.json +components.json +docker/build.sh +docker/caddy/Caddyfile +docker/compose.yml +docker/deploy.sh +docker/images/.gitattributes +docker/save.sh +Dockerfile +drizzle.config.ts +drizzle/0000_overjoyed_strong_guy.sql +drizzle/meta/_journal.json +drizzle/meta/0000_snapshot.json +next.config.mjs +package.json +postcss.config.mjs +public/next.svg +public/vercel.svg +README.md +scripts/generate-data.js +src/app/admin/about/page.tsx +src/app/admin/admin-sidebar.tsx +src/app/admin/charts/printer-error-chart.tsx +src/app/admin/charts/printer-error-rate.tsx +src/app/admin/charts/printer-forecast.tsx +src/app/admin/charts/printer-utilization.tsx +src/app/admin/charts/printer-volume.tsx +src/app/admin/jobs/page.tsx +src/app/admin/layout.tsx +src/app/admin/page.tsx +src/app/admin/printers/columns.tsx +src/app/admin/printers/data-table.tsx +src/app/admin/printers/dialogs/create-printer.tsx +src/app/admin/printers/dialogs/delete-printer.tsx +src/app/admin/printers/dialogs/edit-printer.tsx +src/app/admin/printers/form.tsx +src/app/admin/printers/page.tsx +src/app/admin/settings/download/route.ts +src/app/admin/settings/page.tsx +src/app/admin/users/columns.tsx +src/app/admin/users/data-table.tsx +src/app/admin/users/dialog.tsx +src/app/admin/users/form.tsx +src/app/admin/users/page.tsx +src/app/api/job/[jobId]/remaining-time/route.ts +src/app/api/printers/route.ts +src/app/auth/login/callback/route.ts +src/app/auth/login/route.ts +src/app/globals.css +src/app/job/[jobId]/cancel-form.tsx +src/app/job/[jobId]/edit-comments.tsx +src/app/job/[jobId]/extend-form.tsx +src/app/job/[jobId]/finish-form.tsx +src/app/job/[jobId]/page.tsx +src/app/layout.tsx +src/app/my/jobs/columns.tsx +src/app/my/jobs/data-table.tsx +src/app/my/profile/page.tsx +src/app/not-found.tsx +src/app/page.tsx +src/app/printer/[printerId]/reserve/form.tsx +src/app/printer/[printerId]/reserve/page.tsx +src/components/data-card.tsx +src/components/dynamic-printer-cards.tsx +src/components/header/index.tsx +src/components/header/navigation.tsx +src/components/login-button.tsx +src/components/logout-button.tsx +src/components/personalized-cards.tsx +src/components/printer-availability-badge.tsx +src/components/printer-card/countdown.tsx +src/components/printer-card/index.tsx +src/components/ui/alert-dialog.tsx +src/components/ui/alert.tsx +src/components/ui/avatar.tsx +src/components/ui/badge.tsx +src/components/ui/breadcrumb.tsx +src/components/ui/button.tsx +src/components/ui/card.tsx +src/components/ui/chart.tsx +src/components/ui/dialog.tsx +src/components/ui/dropdown-menu.tsx +src/components/ui/form.tsx +src/components/ui/hover-card.tsx +src/components/ui/input.tsx +src/components/ui/label.tsx +src/components/ui/scroll-area.tsx +src/components/ui/select.tsx +src/components/ui/skeleton.tsx +src/components/ui/sonner.tsx +src/components/ui/table.tsx +src/components/ui/tabs.tsx +src/components/ui/textarea.tsx +src/components/ui/toast.tsx +src/components/ui/toaster.tsx +src/components/ui/use-toast.ts +src/server/actions/authentication/logout.ts +src/server/actions/printers.ts +src/server/actions/printJobs.ts +src/server/actions/timer.ts +src/server/actions/user/delete.ts +src/server/actions/user/update.ts +src/server/actions/users.ts +src/server/auth/index.ts +src/server/auth/oauth.ts +src/server/auth/permissions.ts +src/utils/analytics/error-rate.ts +src/utils/analytics/errors.ts +src/utils/analytics/forecast.ts +src/utils/analytics/utilization.ts +src/utils/analytics/volume.ts +src/utils/drizzle.ts +src/utils/errors.ts +src/utils/fetch.ts +src/utils/guard.ts +src/utils/printers.ts +src/utils/strings.ts +src/utils/styles.ts +tailwind.config.ts +tsconfig.json + +================================================================ +Repository Files +================================================================ + +================ +File: .dockerignore +================ +# Build and utility assets +docker/ +scripts/ + +# Ignore node_modules as they will be installed in the container +node_modules + +# Ignore build artifacts +.next + +# Ignore runtime data +db/ + +# Ignore local configuration files +.env +.env.example + +# Ignore version control files +.git +.gitignore + +# Ignore IDE/editor specific files +*.log +*.tmp +*.DS_Store +.vscode/ +.idea/ + +================ +File: .env.example +================ +# OAuth Configuration +OAUTH_CLIENT_ID=client_id +OAUTH_CLIENT_SECRET=client_secret + +================ +File: .gitignore +================ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# db folder +db/ + +# Env file +.env + + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +================ +File: biome.json +================ +{ + "$schema": "https://biomejs.dev/schemas/1.7.0/schema.json", + "organizeImports": { + "enabled": true + }, + "formatter": { + "enabled": true, + "lineWidth": 120 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "correctness": { + "noUnusedImports": "error" + } + } + } +} + +================ +File: components.json +================ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/utils/styles" + } +} + +================ +File: docker/build.sh +================ +#!/bin/bash + +# Define image name +MYP_RP_IMAGE_NAME="myp-rp" + +# Function to build Docker image +build_image() { + local image_name=$1 + local dockerfile=$2 + local platform=$3 + + echo "Building $image_name Docker image for $platform..." + + docker buildx build --platform $platform -t ${image_name}:latest -f $dockerfile --load . + if [ $? -eq 0 ]; then + echo "$image_name Docker image built successfully" + else + echo "Error occurred while building $image_name Docker image" + exit 1 + fi +} + +# Create and use a builder instance (if not already created) +BUILDER_NAME="myp-rp-arm64-builder" +docker buildx create --name $BUILDER_NAME --use || docker buildx use $BUILDER_NAME + +# Build myp-rp image +build_image "$MYP_RP_IMAGE_NAME" "$PWD/Dockerfile" "linux/arm64" + +# Remove the builder instance +docker buildx rm $BUILDER_NAME + +================ +File: docker/caddy/Caddyfile +================ +{ + debug +} + +m040tbaraspi001.de040.corpintra.net, m040tbaraspi001.de040.corpinter.net { + reverse_proxy myp-rp:3000 + tls internal +} + +================ +File: docker/compose.yml +================ +services: + caddy: + image: caddy:2.8 + container_name: caddy + restart: unless-stopped + ports: + - 80:80 + - 443:443 + volumes: + - ./caddy/data:/data + - ./caddy/config:/config + - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro + myp-rp: + image: myp-rp:latest + container_name: myp-rp + env_file: "/srv/myp-env/github.env" + volumes: + - /srv/MYP-DB:/usr/src/app/db + restart: unless-stopped + +================ +File: docker/deploy.sh +================ +#!/bin/bash + +# Directory containing the Docker images +IMAGE_DIR="docker/images" + +# Load all Docker images from the tar.xz files in the IMAGE_DIR +echo "Loading Docker images from $IMAGE_DIR..." + +for image_file in "$IMAGE_DIR"/*.tar.xz; do + if [ -f "$image_file" ]; then + echo "Loading Docker image from $image_file..." + docker load -i "$image_file" + + # Check if the image loading was successful + if [ $? -ne 0 ]; then + echo "Error occurred while loading Docker image from $image_file" + exit 1 + fi + else + echo "No Docker image tar.xz files found in $IMAGE_DIR." + fi +done + +# Execute docker compose +echo "Running docker compose..." +docker compose -f "docker/compose.yml" up -d + +# Check if the operation was successful +if [ $? -eq 0 ]; then + echo "Docker compose executed successfully" +else + echo "Error occurred while executing docker compose" + exit 1 +fi + +echo "Deployment completed successfully" + +================ +File: docker/images/.gitattributes +================ +caddy_2.8.tar.xz filter=lfs diff=lfs merge=lfs -text +myp-rp_latest.tar.xz filter=lfs diff=lfs merge=lfs -text + +================ +File: docker/save.sh +================ +#!/bin/bash + +# Get image name as argument +IMAGE_NAME=$1 +PLATFORM="linux/arm64" + +# Define paths +IMAGE_DIR="docker/images" +IMAGE_FILE="${IMAGE_DIR}/${IMAGE_NAME//[:\/]/_}.tar" +COMPRESSED_FILE="${IMAGE_FILE}.xz" + +# Function to pull the image +pull_image() { + local image=$1 + if [[ $image == arm64v8/* ]]; then + echo "Pulling image $image without platform specification..." + docker pull $image + else + echo "Pulling image $image for platform $PLATFORM..." + docker pull --platform $PLATFORM $image + fi + return $? +} + +# Pull the image if it is not available locally +if ! docker image inspect ${IMAGE_NAME} &>/dev/null; then + if pull_image ${IMAGE_NAME}; then + echo "Image $IMAGE_NAME pulled successfully." + else + echo "Error occurred while pulling $IMAGE_NAME for platform $PLATFORM" + echo "Trying to pull $IMAGE_NAME without platform specification..." + + # Attempt to pull again without platform + if pull_image ${IMAGE_NAME}; then + echo "Image $IMAGE_NAME pulled successfully without platform." + else + echo "Error occurred while pulling $IMAGE_NAME without platform." + echo "Trying to pull arm64v8/${IMAGE_NAME} instead..." + + # Construct new image name + NEW_IMAGE_NAME="arm64v8/${IMAGE_NAME}" + if pull_image ${NEW_IMAGE_NAME}; then + echo "Image $NEW_IMAGE_NAME pulled successfully." + IMAGE_NAME=${NEW_IMAGE_NAME} # Update IMAGE_NAME to use the new one + else + echo "Error occurred while pulling $NEW_IMAGE_NAME" + exit 1 + fi + fi + fi +else + echo "Image $IMAGE_NAME found locally. Skipping pull." +fi + +# Save the Docker image +echo "Saving $IMAGE_NAME Docker image..." +docker save ${IMAGE_NAME} > $IMAGE_FILE + +# Compress the Docker image (overwriting if file exists) +echo "Compressing $IMAGE_FILE..." +xz -z --force $IMAGE_FILE + +if [ $? -eq 0 ]; then + echo "$IMAGE_NAME Docker image saved and compressed successfully as $COMPRESSED_FILE" +else + echo "Error occurred while compressing $IMAGE_NAME Docker image" + exit 1 +fi + +================ +File: Dockerfile +================ +FROM node:20-bookworm-slim + +# Create application directory +RUN mkdir -p /usr/src/app + +# Set environment variables +ENV PORT=3000 +ENV NEXT_TELEMETRY_DISABLED=1 + +WORKDIR /usr/src/app + +# Copy package.json and pnpm-lock.yaml +COPY package.json /usr/src/app +COPY pnpm-lock.yaml /usr/src/app + +# Install pnpm +RUN corepack enable pnpm + +# Install dependencies +RUN pnpm install + +# Copy the rest of the application code +COPY . /usr/src/app + +# Initialize Database, if it not already exists +RUN pnpm run db + +# Build the application +RUN pnpm run build + +EXPOSE 3000 + +# Start the application +CMD ["/bin/sh", "-c", "if [ ! -f ./db/sqlite.db ]; then pnpm db; fi && pnpm start"] + +================ +File: drizzle.config.ts +================ +import { defineConfig } from "drizzle-kit"; + +//@ts-ignore - better-sqlite driver throws an error even though its an valid value +export default defineConfig({ + dialect: "sqlite", + schema: "./src/server/db/schema.ts", + out: "./drizzle", + driver: "libsql", + dbCredentials: { + url: "file:./db/sqlite.db", + }, +}); + +================ +File: drizzle/0000_overjoyed_strong_guy.sql +================ +CREATE TABLE `printJob` ( + `id` text PRIMARY KEY NOT NULL, + `printerId` text NOT NULL, + `userId` text NOT NULL, + `startAt` integer NOT NULL, + `durationInMinutes` integer NOT NULL, + `comments` text, + `aborted` integer DEFAULT false NOT NULL, + `abortReason` text, + FOREIGN KEY (`printerId`) REFERENCES `printer`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `printer` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `description` text NOT NULL, + `status` integer DEFAULT 0 NOT NULL +); +--> statement-breakpoint +CREATE TABLE `session` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` text NOT NULL, + `expires_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `user` ( + `id` text PRIMARY KEY NOT NULL, + `github_id` integer NOT NULL, + `name` text, + `displayName` text, + `email` text NOT NULL, + `role` text DEFAULT 'guest' +); + +================ +File: drizzle/meta/_journal.json +================ +{ + "version": "6", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1715416514336, + "tag": "0000_overjoyed_strong_guy", + "breakpoints": true + } + ] +} + +================ +File: drizzle/meta/0000_snapshot.json +================ +{ + "version": "6", + "dialect": "sqlite", + "id": "791dc197-5254-4432-bd9f-1368d1a5aa6a", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "printJob": { + "name": "printJob", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "printerId": { + "name": "printerId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "startAt": { + "name": "startAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "durationInMinutes": { + "name": "durationInMinutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "comments": { + "name": "comments", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aborted": { + "name": "aborted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "abortReason": { + "name": "abortReason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "printJob_printerId_printer_id_fk": { + "name": "printJob_printerId_printer_id_fk", + "tableFrom": "printJob", + "tableTo": "printer", + "columnsFrom": [ + "printerId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "printJob_userId_user_id_fk": { + "name": "printJob_userId_user_id_fk", + "tableFrom": "printJob", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "printer": { + "name": "printer", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "github_id": { + "name": "github_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "displayName": { + "name": "displayName", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'guest'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} + +================ +File: next.config.mjs +================ +/** @type {import('next').NextConfig} */ +const nextConfig = { + async headers() { + return [ + { + source: "/:path*", + headers: [ + { + key: "Access-Control-Allow-Origin", + value: "m040tbaraspi001.de040.corpintra.net", + }, + { + key: "Access-Control-Allow-Methods", + value: "GET, POST, PUT, DELETE, OPTIONS", + }, + { + key: "Access-Control-Allow-Headers", + value: "Content-Type, Authorization", + }, + ], + }, + ]; + }, +}; + +export default nextConfig; + +================ +File: package.json +================ +{ + "name": "myp-rp", + "version": "1.0.0", + "private": true, + "packageManager": "pnpm@9.12.1", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "db:create-default": "mkdir -p db/", + "db:generate-sqlite": "pnpm drizzle-kit generate", + "db:clean": "rm -rf db/ drizzle/", + "db:migrate": "pnpm drizzle-kit migrate", + "db": "pnpm db:create-default && pnpm db:generate-sqlite && pnpm db:migrate", + "db:reset": "pnpm db:clean && pnpm db" + }, + "dependencies": { + "@faker-js/faker": "^9.2.0", + "@headlessui/react": "^2.1.10", + "@headlessui/tailwindcss": "^0.2.1", + "@hookform/resolvers": "^3.9.0", + "@libsql/client": "^0.14.0", + "@lucia-auth/adapter-drizzle": "^1.1.0", + "@radix-ui/react-alert-dialog": "^1.1.2", + "@radix-ui/react-avatar": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.2", + "@radix-ui/react-dropdown-menu": "^2.1.2", + "@radix-ui/react-hover-card": "^1.1.2", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-scroll-area": "^1.2.0", + "@radix-ui/react-select": "^2.1.2", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.1", + "@radix-ui/react-toast": "^1.2.2", + "@remixicon/react": "^4.3.0", + "@tanstack/react-table": "^8.20.5", + "@tremor/react": "^3.18.3", + "arctic": "^1.9.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "drizzle-orm": "^0.30.10", + "lodash": "^4.17.21", + "lucia": "^3.2.1", + "lucide-react": "^0.378.0", + "luxon": "^3.5.0", + "next": "14.2.3", + "next-themes": "^0.3.0", + "oslo": "^1.2.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.53.0", + "react-if": "^4.1.5", + "react-timer-hook": "^3.0.7", + "recharts": "^2.13.3", + "regression": "^2.0.1", + "sonner": "^1.5.0", + "sqlite": "^5.1.1", + "sqlite3": "^5.1.7", + "swr": "^2.2.5", + "tailwind-merge": "^2.5.3", + "tailwindcss-animate": "^1.0.7", + "use-debounce": "^10.0.3", + "uuid": "^11.0.2", + "zod": "^3.23.8" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.3", + "@tailwindcss/forms": "^0.5.9", + "@types/lodash": "^4.17.13", + "@types/luxon": "^3.4.2", + "@types/node": "^20.16.11", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", + "drizzle-kit": "^0.21.4", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.13", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + } +} + +================ +File: postcss.config.mjs +================ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; + +================ +File: public/next.svg +================ + + +================ +File: public/vercel.svg +================ + + +================ +File: README.md +================ +# MYP - Manage Your Printer + +MYP (Manage Your Printer) ist eine Webanwendung zur Reservierung von 3D-Druckern. +Sie wurde im Rahmen des Abschlussprojektes der Fachinformatiker Ausbildung für Daten- und Prozessanalyse für die Technische Berufsausbildung des Mercedes-Benz Werkes Berlin-Marienfelde entwickelt. + +## Deployment + +### Voraussetzungen + +- Netzwerk auf Raspberry Pi ist eingerichtet +- Docker ist installiert + +### Schritte + +1. Docker-Container bauen (docker/build.sh) +2. Docker-Container speichern (docker/save.sh caddy:2.8 myp-rp:latest) +3. Docker-Container auf Raspberry Pi bereitstellen (docker/deploy.sh) + +## Entwicklerinformationen + +### Raspberry Pi Einstellungen + +Auf dem Raspberry Pi wurde Raspbian Lite installiert. +Unter /srv/* sind die Projektdateien zu finden. + +### Anmeldedaten + +``` +Benutzer: myp +Passwort: (persönlich bekannt) +``` + +================ +File: scripts/generate-data.js +================ +const sqlite3 = require("sqlite3"); +const faker = require("@faker-js/faker").faker; +const { random, sample, sampleSize, sum } = require("lodash"); +const { DateTime } = require("luxon"); +const { open } = require("sqlite"); +const { v4: uuidv4 } = require("uuid"); + +const dbPath = "./db/sqlite.db"; + +// Configuration for test data generation +let startDate = DateTime.fromISO("2024-10-08"); +let endDate = DateTime.fromISO("2024-11-08"); +let numberOfPrinters = 5; + +// Use weekday names for better readability and ease of setting trends +let avgPrintTimesPerDay = { + Monday: 4, + Tuesday: 2, + Wednesday: 5, + Thursday: 2, + Friday: 3, + Saturday: 0, + Sunday: 0, +}; // Average number of prints for each weekday + +let avgPrintDurationPerDay = { + Monday: 240, // Total average duration in minutes for Monday + Tuesday: 30, + Wednesday: 45, + Thursday: 40, + Friday: 120, + Saturday: 0, + Sunday: 0, +}; // Average total duration of prints for each weekday + +let printerUsage = { + "Drucker 1": 0.5, + "Drucker 2": 0.7, + "Drucker 3": 0.6, + "Drucker 4": 0.3, + "Drucker 5": 0.4, +}; // Usage percentages for each printer + +// **New Configurations for Error Rates** +let generalErrorRate = 0.05; // 5% chance any print job may fail +let printerErrorRates = { + "Drucker 1": 0.02, // 2% error rate for Printer 1 + "Drucker 2": 0.03, + "Drucker 3": 0.01, + "Drucker 4": 0.05, + "Drucker 5": 0.04, +}; // Error rates for each printer + +const holidays = []; // Example holidays +const existingJobs = []; + +const initDB = async () => { + console.log("Initializing database connection..."); + return open({ + filename: dbPath, + driver: sqlite3.Database, + }); +}; + +const createUser = (isPowerUser = false) => { + const name = [faker.person.firstName(), faker.person.lastName()]; + + const user = { + id: uuidv4(), + github_id: faker.number.int(), + username: `${name[0].slice(0, 2)}${name[1].slice(0, 6)}`.toUpperCase(), + displayName: `${name[0]} ${name[1]}`.toUpperCase(), + email: `${name[0]}.${name[1]}@example.com`, + role: sample(["user", "admin"]), + isPowerUser, + }; + console.log("Created user:", user); + return user; +}; + +const createPrinter = (index) => { + const printer = { + id: uuidv4(), + name: `Drucker ${index}`, + description: faker.lorem.sentence(), + status: random(0, 2), + }; + console.log("Created printer:", printer); + return printer; +}; + +const isPrinterAvailable = (printer, startAt, duration) => { + const endAt = startAt + duration * 60 * 1000; // Convert minutes to milliseconds + return !existingJobs.some((job) => { + const jobStart = job.startAt; + const jobEnd = job.startAt + job.durationInMinutes * 60 * 1000; + return ( + printer.id === job.printerId && + ((startAt >= jobStart && startAt < jobEnd) || + (endAt > jobStart && endAt <= jobEnd) || + (startAt <= jobStart && endAt >= jobEnd)) + ); + }); +}; + +const createPrintJob = (users, printers, startAt, duration) => { + const user = sample(users); + let printer; + + // Weighted selection based on printer usage + const printerNames = Object.keys(printerUsage); + const weightedPrinters = printers.filter((p) => printerNames.includes(p.name)); + + // Create a weighted array of printers based on usage percentages + const printerWeights = weightedPrinters.map((p) => ({ + printer: p, + weight: printerUsage[p.name], + })); + + const totalWeight = sum(printerWeights.map((pw) => pw.weight)); + const randomWeight = Math.random() * totalWeight; + let accumulatedWeight = 0; + for (const pw of printerWeights) { + accumulatedWeight += pw.weight; + if (randomWeight <= accumulatedWeight) { + printer = pw.printer; + break; + } + } + + if (!printer) { + printer = sample(printers); + } + + if (!isPrinterAvailable(printer, startAt, duration)) { + console.log("Printer not available, skipping job creation."); + return null; + } + + // **Determine if the job should be aborted based on error rates** + let aborted = false; + let abortReason = null; + + // Calculate the combined error rate + const printerErrorRate = printerErrorRates[printer.name] || 0; + const combinedErrorRate = 1 - (1 - generalErrorRate) * (1 - printerErrorRate); + + if (Math.random() < combinedErrorRate) { + aborted = true; + const errorMessages = [ + "Unbekannt", + "Keine Ahnung", + "Falsch gebucht", + "Filament gelöst", + "Druckabbruch", + "Düsenverstopfung", + "Schichthaftung fehlgeschlagen", + "Materialmangel", + "Dateifehler", + "Temperaturproblem", + "Mechanischer Fehler", + "Softwarefehler", + "Kalibrierungsfehler", + "Überhitzung", + ]; + abortReason = sample(errorMessages); // Generate a random abort reason + } + + const printJob = { + id: uuidv4(), + printerId: printer.id, + userId: user.id, + startAt, + durationInMinutes: duration, + comments: faker.lorem.sentence(), + aborted, + abortReason, + }; + console.log("Created print job:", printJob); + return printJob; +}; + +const generatePrintJobsForDay = async (users, printers, dayDate, totalJobsForDay, totalDurationForDay, db, dryRun) => { + console.log(`Generating print jobs for ${dayDate.toISODate()}...`); + + // Generate random durations that sum up approximately to totalDurationForDay + const durations = []; + let remainingDuration = totalDurationForDay; + for (let i = 0; i < totalJobsForDay; i++) { + const avgJobDuration = remainingDuration / (totalJobsForDay - i); + const jobDuration = Math.max( + Math.round(random(avgJobDuration * 0.8, avgJobDuration * 1.2)), + 5, // Minimum duration of 5 minutes + ); + durations.push(jobDuration); + remainingDuration -= jobDuration; + } + + // Shuffle durations to randomize job lengths + const shuffledDurations = sampleSize(durations, durations.length); + + for (let i = 0; i < totalJobsForDay; i++) { + const duration = shuffledDurations[i]; + + // Random start time between 8 AM and 6 PM, adjusted to avoid overlapping durations + const possibleStartHours = Array.from({ length: 10 }, (_, idx) => idx + 8); // 8 AM to 6 PM + let startAt; + let attempts = 0; + do { + const hour = sample(possibleStartHours); + const minute = random(0, 59); + startAt = dayDate.set({ hour, minute, second: 0, millisecond: 0 }).toMillis(); + attempts++; + if (attempts > 10) { + console.log("Unable to find available time slot, skipping job."); + break; + } + } while (!isPrinterAvailable(sample(printers), startAt, duration)); + + if (attempts > 10) continue; + + const printJob = createPrintJob(users, printers, startAt, duration); + if (printJob) { + if (!dryRun) { + await db.run( + `INSERT INTO printJob (id, printerId, userId, startAt, durationInMinutes, comments, aborted, abortReason) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + [ + printJob.id, + printJob.printerId, + printJob.userId, + printJob.startAt, + printJob.durationInMinutes, + printJob.comments, + printJob.aborted ? 1 : 0, + printJob.abortReason, + ], + ); + } + existingJobs.push(printJob); + console.log("Inserted print job into database:", printJob.id); + } + } +}; + +const generateTestData = async (dryRun = false) => { + console.log("Starting test data generation..."); + const db = await initDB(); + + // Generate users and printers + const users = [ + ...Array.from({ length: 7 }, () => createUser(false)), + ...Array.from({ length: 3 }, () => createUser(true)), + ]; + const printers = Array.from({ length: numberOfPrinters }, (_, index) => createPrinter(index + 1)); + + if (!dryRun) { + // Insert users into the database + for (const user of users) { + await db.run( + `INSERT INTO user (id, github_id, name, displayName, email, role) + VALUES (?, ?, ?, ?, ?, ?)`, + [user.id, user.github_id, user.username, user.displayName, user.email, user.role], + ); + console.log("Inserted user into database:", user.id); + } + + // Insert printers into the database + for (const printer of printers) { + await db.run( + `INSERT INTO printer (id, name, description, status) + VALUES (?, ?, ?, ?)`, + [printer.id, printer.name, printer.description, printer.status], + ); + console.log("Inserted printer into database:", printer.id); + } + } + + // Generate print jobs for each day within the specified date range + let currentDay = startDate; + while (currentDay <= endDate) { + const weekdayName = currentDay.toFormat("EEEE"); // Get weekday name (e.g., 'Monday') + if (holidays.includes(currentDay.toISODate()) || avgPrintTimesPerDay[weekdayName] === 0) { + console.log(`Skipping holiday or no jobs scheduled: ${currentDay.toISODate()}`); + currentDay = currentDay.plus({ days: 1 }); + continue; + } + + const totalJobsForDay = avgPrintTimesPerDay[weekdayName]; + const totalDurationForDay = avgPrintDurationPerDay[weekdayName]; + + await generatePrintJobsForDay(users, printers, currentDay, totalJobsForDay, totalDurationForDay, db, dryRun); + currentDay = currentDay.plus({ days: 1 }); + } + + if (!dryRun) { + await db.close(); + console.log("Database connection closed. Test data generation complete."); + } else { + console.log("Dry run complete. No data was written to the database."); + } +}; + +const setConfigurations = (config) => { + if (config.startDate) startDate = DateTime.fromISO(config.startDate); + if (config.endDate) endDate = DateTime.fromISO(config.endDate); + if (config.numberOfPrinters) numberOfPrinters = config.numberOfPrinters; + if (config.avgPrintTimesPerDay) avgPrintTimesPerDay = config.avgPrintTimesPerDay; + if (config.avgPrintDurationPerDay) avgPrintDurationPerDay = config.avgPrintDurationPerDay; + if (config.printerUsage) printerUsage = config.printerUsage; + if (config.generalErrorRate !== undefined) generalErrorRate = config.generalErrorRate; + if (config.printerErrorRates) printerErrorRates = config.printerErrorRates; +}; + +// Example usage +setConfigurations({ + startDate: "2024-10-08", + endDate: "2024-11-08", + numberOfPrinters: 6, + avgPrintTimesPerDay: { + Monday: 4, // High usage + Tuesday: 2, // Low usage + Wednesday: 3, // Low usage + Thursday: 2, // Low usage + Friday: 8, // High usage + Saturday: 0, + Sunday: 0, + }, + avgPrintDurationPerDay: { + Monday: 300, // High total duration + Tuesday: 60, // Low total duration + Wednesday: 90, + Thursday: 60, + Friday: 240, + Saturday: 0, + Sunday: 0, + }, + printerUsage: { + "Drucker 1": 2.3, + "Drucker 2": 1.7, + "Drucker 3": 0.1, + "Drucker 4": 1.5, + "Drucker 5": 2.4, + "Drucker 6": 0.3, + "Drucker 7": 0.9, + "Drucker 8": 0.1, + }, + generalErrorRate: 0.05, // 5% general error rate + printerErrorRates: { + "Drucker 1": 0.02, + "Drucker 2": 0.03, + "Drucker 3": 0.1, + "Drucker 4": 0.05, + "Drucker 5": 0.04, + "Drucker 6": 0.02, + "Drucker 7": 0.01, + "PrinteDrucker 8": 0.03, + }, +}); + +generateTestData(process.argv.includes("--dry-run")) + .then(() => { + console.log("Test data generation script finished."); + }) + .catch((err) => { + console.error("Error generating test data:", err); + }); + +================ +File: src/app/admin/about/page.tsx +================ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Über MYP", +}; + +export default async function AdminPage() { + return ( + + + Über MYP + + MYP — Manage Your Printer + + + +

+ MYP ist eine Webanwendung zur Reservierung von 3D-Druckern. Sie wurde im Rahmen des + Abschlussprojektes der Fachinformatiker Ausbildung für Daten- und Prozessanalyse für die Technische + Berufsausbildung des Mercedes-Benz Werkes Berlin-Marienfelde entwickelt. +

+

+ © 2024{" "} + + Torben Haack + +

+
+
+ ); +} + +================ +File: src/app/admin/admin-sidebar.tsx +================ +"use client"; + +import { cn } from "@/utils/styles"; +import { FileIcon, HeartIcon, LayoutDashboardIcon, PrinterIcon, UsersIcon, WrenchIcon } from "lucide-react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +interface AdminSite { + name: string; + path: string; + icon: React.ReactNode; +} + +export function AdminSidebar() { + const pathname = usePathname(); + const adminSites: AdminSite[] = [ + { + name: "Dashboard", + path: "/admin", + icon: , + }, + { + name: "Benutzer", + path: "/admin/users", + icon: , + }, + { + name: "Drucker", + path: "/admin/printers", + icon: , + }, + { + name: "Druckaufträge", + path: "/admin/jobs", + icon: , + }, + { + name: "Einstellungen", + path: "/admin/settings", + icon: , + }, + { + name: "Über MYP", + path: "/admin/about", + icon: , + }, + ]; + + return ( +
    + {adminSites.map((site) => ( +
  • + + {site.icon} + {site.name} + +
  • + ))} +
+ ); +} + +================ +File: src/app/admin/charts/printer-error-chart.tsx +================ +"use client"; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; +import { Bar, BarChart, CartesianGrid, LabelList, XAxis, YAxis } from "recharts"; + +export const description = "Ein Säulendiagramm zur Darstellung der Abbruchgründe und ihrer Häufigkeit"; + +interface AbortReasonCountChartProps { + abortReasonCount: { + abortReason: string; + count: number; + }[]; +} + +const chartConfig = { + abortReason: { + label: "Abbruchgrund", + }, +} satisfies ChartConfig; + +export function AbortReasonCountChart({ abortReasonCount }: AbortReasonCountChartProps) { + // Transform data to fit the chart structure + const chartData = abortReasonCount.map((reason) => ({ + abortReason: reason.abortReason, + count: reason.count, + })); + + return ( + + + Abbruchgründe + Häufigkeit der Abbruchgründe für Druckaufträge + + + + + + value} + /> + `${value}`} /> + } /> + + `${value}`} + /> + + + + + + ); +} + +================ +File: src/app/admin/charts/printer-error-rate.tsx +================ +"use client"; +import { Bar, BarChart, CartesianGrid, LabelList, XAxis, YAxis } from "recharts"; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; +import type { PrinterErrorRate } from "@/utils/analytics/error-rate"; + +export const description = "Ein Säulendiagramm zur Darstellung der Fehlerrate"; + +interface PrinterErrorRateChartProps { + printerErrorRate: PrinterErrorRate[]; +} + +const chartConfig = { + errorRate: { + label: "Fehlerrate", + }, +} satisfies ChartConfig; + +export function PrinterErrorRateChart({ printerErrorRate }: PrinterErrorRateChartProps) { + // Transform data to fit the chart structure + const chartData = printerErrorRate.map((printer) => ({ + printer: printer.name, + errorRate: printer.errorRate, + })); + + return ( + + + Fehlerrate + Fehlerrate der Drucker in Prozent + + + + + + value} + /> + `${value}%`} /> + } /> + + `${value}%`} + /> + + + + + + ); +} + +================ +File: src/app/admin/charts/printer-forecast.tsx +================ +"use client"; + +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; +import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; + +export const description = "Ein Bereichsdiagramm zur Darstellung der prognostizierten Nutzung pro Wochentag"; + +interface ForecastData { + day: number; // 0 for Sunday, 1 for Monday, ..., 6 for Saturday + usageMinutes: number; +} + +interface ForecastChartProps { + forecastData: ForecastData[]; +} + +const chartConfig = { + usage: { + label: "Prognostizierte Nutzung", + color: "hsl(var(--chart-1))", + }, +} satisfies ChartConfig; + +const daysOfWeek = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"]; + +export function ForecastPrinterUsageChart({ forecastData }: ForecastChartProps) { + // Transform and slice data to fit the chart structure + const chartData = forecastData.map((data) => ({ + //slice(1, forecastData.length - 1). + day: daysOfWeek[data.day], // Map day number to weekday name + usage: data.usageMinutes, + })); + + return ( + + + Prognostizierte Nutzung pro Wochentag + + + + + + + + } /> + + + + + +
+ Zeigt die prognostizierte Nutzungszeit pro Wochentag in Minuten. +
+
+ Besten Tage zur Wartung: {bestMaintenanceDays(forecastData)} +
+
+
+ ); +} + +function bestMaintenanceDays(forecastData: ForecastData[]) { + const sortedData = forecastData.map((a) => a).sort((a, b) => a.usageMinutes - b.usageMinutes); // Sort ascending + + const q1Index = Math.floor(sortedData.length * 0.33); + const q1 = sortedData[q1Index].usageMinutes; // First quartile (Q1) value + + const filteredData = sortedData.filter((data) => data.usageMinutes <= q1); + + return filteredData + .map((data) => { + const days = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"]; + return days[data.day]; + }) + .join(", "); +} + +================ +File: src/app/admin/charts/printer-utilization.tsx +================ +"use client"; + +import { TrendingUp } from "lucide-react"; +import * as React from "react"; +import { Label, Pie, PieChart } from "recharts"; + +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; + +export const description = "Nutzung des Druckers"; + +interface ComponentProps { + data: { + printerId: string; + utilizationPercentage: number; + name: string; + }; +} + +const chartConfig = {} satisfies ChartConfig; + +export function PrinterUtilizationChart({ data }: ComponentProps) { + const totalUtilization = React.useMemo(() => data.utilizationPercentage, [data]); + const dataWithColor = { + ...data, + fill: "rgb(34 197 94)", + }; + const free = { + printerId: "-", + utilizationPercentage: 1 - data.utilizationPercentage, + name: "(Frei)", + fill: "rgb(212 212 212)", + }; + + return ( + + + {data.name} + Nutzung des ausgewählten Druckers + + + + + } /> + + + + + + +
+ Übersicht der Nutzung +
+
Aktuelle Auslastung des Druckers
+
+
+ ); +} + +================ +File: src/app/admin/charts/printer-volume.tsx +================ +"use client"; +import { Bar, BarChart, CartesianGrid, LabelList, XAxis } from "recharts"; + +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; + +export const description = "Ein Balkendiagramm mit Beschriftung"; + +interface PrintVolumes { + today: number; + thisWeek: number; + thisMonth: number; +} + +const chartConfig = { + volume: { + label: "Volumen", + }, +} satisfies ChartConfig; + +interface PrinterVolumeChartProps { + printerVolume: PrintVolumes; +} + +export function PrinterVolumeChart({ printerVolume }: PrinterVolumeChartProps) { + const chartData = [ + { period: "Heute", volume: printerVolume.today, color: "hsl(var(--chart-1))" }, + { period: "Diese Woche", volume: printerVolume.thisWeek, color: "hsl(var(--chart-2))" }, + { period: "Diesen Monat", volume: printerVolume.thisMonth, color: "hsl(var(--chart-3))" }, + ]; + + return ( + + + Druckvolumen + Vergleich: Heute, Diese Woche, Diesen Monat + + + + + + value} + /> + } /> + + + + + + + +
+ Zeigt das Druckvolumen für heute, diese Woche und diesen Monat +
+
+
+ ); +} + +================ +File: src/app/admin/jobs/page.tsx +================ +import { columns } from "@/app/my/jobs/columns"; +import { JobsTable } from "@/app/my/jobs/data-table"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { db } from "@/server/db"; +import { printJobs } from "@/server/db/schema"; +import { desc } from "drizzle-orm"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Alle Druckaufträge", +}; + +export default async function AdminJobsPage() { + const allJobs = await db.query.printJobs.findMany({ + orderBy: [desc(printJobs.startAt)], + with: { + user: true, + printer: true, + }, + }); + + return ( + + +
+ Druckaufträge + Alle Druckaufträge +
+
+ + + +
+ ); +} + +================ +File: src/app/admin/layout.tsx +================ +import { AdminSidebar } from "@/app/admin/admin-sidebar"; +import { validateRequest } from "@/server/auth"; +import { UserRole } from "@/server/auth/permissions"; +import { IS_NOT, guard } from "@/utils/guard"; +import { redirect } from "next/navigation"; + +interface AdminLayoutProps { + children: React.ReactNode; +} + +export const dynamic = "force-dynamic"; + +export default async function AdminLayout(props: AdminLayoutProps) { + const { children } = props; + const { user } = await validateRequest(); + + if (guard(user, IS_NOT, UserRole.ADMIN)) { + redirect("/"); + } + + return ( +
+
+

Admin

+
+
+ +
{children}
+
+
+ ); +} + +================ +File: src/app/admin/page.tsx +================ +import { AbortReasonCountChart } from "@/app/admin/charts/printer-error-chart"; +import { PrinterErrorRateChart } from "@/app/admin/charts/printer-error-rate"; +import { ForecastPrinterUsageChart } from "@/app/admin/charts/printer-forecast"; +import { PrinterUtilizationChart } from "@/app/admin/charts/printer-utilization"; +import { PrinterVolumeChart } from "@/app/admin/charts/printer-volume"; +import { DataCard } from "@/components/data-card"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { db } from "@/server/db"; +import { calculatePrinterErrorRate } from "@/utils/analytics/error-rate"; +import { calculateAbortReasonsCount } from "@/utils/analytics/errors"; +import { forecastPrinterUsage } from "@/utils/analytics/forecast"; +import { calculatePrinterUtilization } from "@/utils/analytics/utilization"; +import { calculatePrintVolumes } from "@/utils/analytics/volume"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Admin Dashboard", +}; + +export const dynamic = "force-dynamic"; + +export default async function AdminPage() { + const currentDate = new Date(); + + const lastMonth = new Date(); + lastMonth.setDate(currentDate.getDate() - 31); + const printers = await db.query.printers.findMany({}); + const printJobs = await db.query.printJobs.findMany({ + where: (job, { gte }) => gte(job.startAt, lastMonth), + with: { + printer: true, + }, + }); + if (printJobs.length < 1) { + return ( + + + Druckaufträge + Zurzeit sind keine Druckaufträge verfügbar. + + +

Aktualisieren Sie die Seite oder prüfen Sie später erneut, ob neue Druckaufträge verfügbar sind.

+
+
+ ); + } + + const currentPrintJobs = printJobs.filter((job) => { + if (job.aborted) return false; + + const endAt = job.startAt.getTime() + job.durationInMinutes * 1000 * 60; + + return endAt > currentDate.getTime(); + }); + const occupiedPrinters = currentPrintJobs.map((job) => job.printer.id); + const freePrinters = printers.filter((printer) => !occupiedPrinters.includes(printer.id)); + const printerUtilization = calculatePrinterUtilization(printJobs); + const printerVolume = calculatePrintVolumes(printJobs); + const printerAbortReasons = calculateAbortReasonsCount(printJobs); + const printerErrorRate = calculatePrinterErrorRate(printJobs); + const printerForecast = forecastPrinterUsage(printJobs); + + return ( + <> + + + Allgemein + Druckerauslastung + Fehlerberichte + Prognosen + + +
+
+ +
+ + +
+
+ +
+
+ +
+ {printerUtilization.map((data) => ( + + ))} +
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ ({ + day: index, + usageMinutes, + }))} + /> +
+
+
+
+ + ); +} + +================ +File: src/app/admin/printers/columns.tsx +================ +"use client"; +import type { printers } from "@/server/db/schema"; +import type { ColumnDef } from "@tanstack/react-table"; +import type { InferSelectModel } from "drizzle-orm"; +import { ArrowUpDown, MoreHorizontal, PencilIcon } from "lucide-react"; + +import { EditPrinterDialogContent, EditPrinterDialogTrigger } from "@/app/admin/printers/dialogs/edit-printer"; +import { Button } from "@/components/ui/button"; +import { Dialog } from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { type PrinterStatus, translatePrinterStatus } from "@/utils/printers"; +import { useState } from "react"; + +// This type is used to define the shape of our data. +// You can use a Zod schema here if you want. + +export const columns: ColumnDef>[] = [ + { + accessorKey: "id", + header: ({ column }) => { + return ( + + ); + }, + }, + { + accessorKey: "name", + header: "Name", + }, + { + accessorKey: "description", + header: "Beschreibung", + }, + { + accessorKey: "status", + header: "Status", + cell: ({ row }) => { + const status = row.getValue("status"); + const translated = translatePrinterStatus(status as PrinterStatus); + + return translated; + }, + }, + { + id: "actions", + cell: ({ row }) => { + const printer = row.original; + const [open, setOpen] = useState(false); + + return ( + + + + + + + Aktionen + ABC + + +
+ + Bearbeiten +
+
+
+
+
+ +
+ ); + }, + }, +]; + +================ +File: src/app/admin/printers/data-table.tsx +================ +"use client"; + +import { + type ColumnDef, + type ColumnFiltersState, + type SortingState, + type VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { SlidersHorizontalIcon } from "lucide-react"; +import { useState } from "react"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; +} + +export function DataTable({ columns, data }: DataTableProps) { + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); + const [columnVisibility, setColumnVisibility] = useState({}); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setColumnFilters, + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + state: { + sorting, + columnFilters, + columnVisibility, + }, + }); + + return ( +
+
+ table.getColumn("name")?.setFilterValue(event.target.value)} + className="max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + column.toggleVisibility(!!value)} + > + {column.id} + + ); + })} + + +
+ +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} + + )) + ) : ( + + + Keine Ergebnisse gefunden. + + + )} + +
+
+
+ + +
+
+ ); +} + +================ +File: src/app/admin/printers/dialogs/create-printer.tsx +================ +"use client"; + +import { PrinterForm } from "@/app/admin/printers/form"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { useState } from "react"; + +interface CreatePrinterDialogProps { + children: React.ReactNode; +} + +export function CreatePrinterDialog(props: CreatePrinterDialogProps) { + const { children } = props; + const [open, setOpen] = useState(false); + + return ( + + {children} + + + Drucker erstellen + + + + + ); +} + +================ +File: src/app/admin/printers/dialogs/delete-printer.tsx +================ +"use client"; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { useToast } from "@/components/ui/use-toast"; +import { deletePrinter } from "@/server/actions/printers"; +import { TrashIcon } from "lucide-react"; + +interface DeletePrinterDialogProps { + printerId: string; + setOpen: (state: boolean) => void; +} +export function DeletePrinterDialog(props: DeletePrinterDialogProps) { + const { printerId, setOpen } = props; + const { toast } = useToast(); + + async function onSubmit() { + toast({ + description: "Drucker wird gelöscht...", + }); + try { + const result = await deletePrinter(printerId); + if (result?.error) { + toast({ + description: result.error, + variant: "destructive", + }); + } + toast({ + description: "Drucker wurde gelöscht.", + }); + setOpen(false); + } catch (error) { + if (error instanceof Error) { + toast({ + description: error.message, + variant: "destructive", + }); + } else { + toast({ + description: "Ein unbekannter Fehler ist aufgetreten.", + variant: "destructive", + }); + } + } + } + + return ( + + + + + + + Bist Du dir sicher? + + Diese Aktion kann nicht rückgängig gemacht werden. Der Drucker und die damit verbundenen Daten werden + unwiderruflich gelöscht. + + + + Abbrechen + + Ja, löschen + + + + + ); +} + +================ +File: src/app/admin/printers/dialogs/edit-printer.tsx +================ +import { PrinterForm } from "@/app/admin/printers/form"; +import { DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import type { InferResultType } from "@/utils/drizzle"; + +interface EditPrinterDialogTriggerProps { + children: React.ReactNode; +} + +export function EditPrinterDialogTrigger(props: EditPrinterDialogTriggerProps) { + const { children } = props; + + return {children}; +} + +interface EditPrinterDialogContentProps { + printer: InferResultType<"printers">; + setOpen: (open: boolean) => void; +} +export function EditPrinterDialogContent(props: EditPrinterDialogContentProps) { + const { printer, setOpen } = props; + + return ( + + + Drucker bearbeiten + + + + ); +} + +================ +File: src/app/admin/printers/form.tsx +================ +"use client"; +import { DeletePrinterDialog } from "@/app/admin/printers/dialogs/delete-printer"; +import { Button } from "@/components/ui/button"; +import { DialogClose } from "@/components/ui/dialog"; +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { useToast } from "@/components/ui/use-toast"; +import { createPrinter, updatePrinter } from "@/server/actions/printers"; +import type { InferResultType } from "@/utils/drizzle"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { SaveIcon, XCircleIcon } from "lucide-react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +export const formSchema = z.object({ + name: z + .string() + .min(2, { + message: "Der Name muss mindestens 2 Zeichen lang sein.", + }) + .max(50), + description: z + .string() + .min(2, { + message: "Die Beschreibung muss mindestens 2 Zeichen lang sein.", + }) + .max(50), + status: z.coerce.number().int().min(0).max(1), +}); + +interface PrinterFormProps { + printer?: InferResultType<"printers">; + setOpen: (state: boolean) => void; +} + +export function PrinterForm(props: PrinterFormProps) { + const { printer, setOpen } = props; + const { toast } = useToast(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: printer?.name ?? "", + description: printer?.description ?? "", + status: printer?.status ?? 0, + }, + }); + + // 2. Define a submit handler. + async function onSubmit(values: z.infer) { + // TODO: create or update + if (printer) { + toast({ + description: "Drucker wird aktualisiert...", + }); + + // Update + try { + const result = await updatePrinter(printer.id, { + description: values.description, + name: values.name, + status: values.status, + }); + if (result?.error) { + toast({ + description: result.error, + variant: "destructive", + }); + } + + setOpen(false); + + toast({ + description: "Drucker wurde aktualisiert.", + variant: "default", + }); + } catch (error) { + if (error instanceof Error) { + toast({ + description: error.message, + variant: "destructive", + }); + } else { + toast({ + description: "Ein unbekannter Fehler ist aufgetreten.", + variant: "destructive", + }); + } + } + } else { + toast({ + description: "Drucker wird erstellt...", + variant: "default", + }); + + // Create + try { + const result = await createPrinter({ + description: values.description, + name: values.name, + status: values.status, + }); + if (result?.error) { + toast({ + description: result.error, + variant: "destructive", + }); + } + + setOpen(false); + + toast({ + description: "Drucker wurde erstellt.", + variant: "default", + }); + } catch (error) { + if (error instanceof Error) { + toast({ + description: error.message, + variant: "destructive", + }); + } else { + toast({ + description: "Ein unbekannter Fehler ist aufgetreten.", + variant: "destructive", + }); + } + } + } + } + + return ( +
+ + ( + + Name + + + + Bitte gib einen eindeutigen Namen für den Drucker ein. + + + )} + /> + ( + + Beschreibung + + + + Füge eine kurze Beschreibung des Druckers hinzu. + + + )} + /> + ( + + Status + + Wähle den aktuellen Status des Druckers. + + + )} + /> +
+ {printer && } + {!printer && ( + + + + )} + +
+ + + ); +} + +================ +File: src/app/admin/printers/page.tsx +================ +import { columns } from "@/app/admin/printers/columns"; +import { DataTable } from "@/app/admin/printers/data-table"; +import { CreatePrinterDialog } from "@/app/admin/printers/dialogs/create-printer"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { db } from "@/server/db"; +import { PlusCircleIcon } from "lucide-react"; + +export default async function AdminPage() { + const data = await db.query.printers.findMany(); + + return ( + + +
+ Druckerverwaltung + Suche, Bearbeite, Lösche und Erstelle Drucker +
+ + + +
+ + + +
+ ); +} + +================ +File: src/app/admin/settings/download/route.ts +================ +import fs from "node:fs"; + +export const dynamic = 'force-dynamic'; + +export async function GET() { + return new Response(fs.readFileSync("./db/sqlite.db")); +} + +================ +File: src/app/admin/settings/page.tsx +================ +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import Link from "next/link"; + +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Systemeinstellungen", +}; + +export default function AdminPage() { + return ( + + + Einstellungen + Systemeinstellungen + + +
+

Datenbank herunterladen

+ +
+
+
+ ); +} + +================ +File: src/app/admin/users/columns.tsx +================ +"use client"; + +import { type UserRole, translateUserRole } from "@/server/auth/permissions"; +import type { users } from "@/server/db/schema"; +import type { ColumnDef } from "@tanstack/react-table"; +import type { InferSelectModel } from "drizzle-orm"; +import { + ArrowUpDown, + MailIcon, + MessageCircleIcon, + MoreHorizontal, +} from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import Link from "next/link"; +import { + EditUserDialogContent, + EditUserDialogRoot, + EditUserDialogTrigger, +} from "@/app/admin/users/dialog"; + +// This type is used to define the shape of our data. +// You can use a Zod schema here if you want. +export type User = { + id: string; + github_id: number; + username: string; + displayName: string; + email: string; + role: string; +}; + +export const columns: ColumnDef>[] = [ + { + accessorKey: "id", + header: ({ column }) => { + return ( + + ); + }, + }, + { + accessorKey: "github_id", + header: "GitHub ID", + }, + { + accessorKey: "username", + header: "Username", + }, + { + accessorKey: "displayName", + header: "Name", + }, + { + accessorKey: "email", + header: "E-Mail", + }, + { + accessorKey: "role", + header: "Rolle", + cell: ({ row }) => { + const role = row.getValue("role"); + const translated = translateUserRole(role as UserRole); + + return translated; + }, + }, + { + id: "actions", + cell: ({ row }) => { + const user = row.original; + + return ( + + + + + + + Aktionen + + + + Teams-Chat öffnen + + + + + + E-Mail schicken + + + + + + + + + + + ); + }, + }, +]; + +function generateTeamsChatURL(email: string) { + return `https://teams.microsoft.com/l/chat/0/0?users=${email}`; +} + +function generateEMailURL(email: string) { + return `mailto:${email}`; +} + +================ +File: src/app/admin/users/data-table.tsx +================ +"use client"; + +import { + type ColumnDef, + type ColumnFiltersState, + type SortingState, + type VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { SlidersHorizontalIcon } from "lucide-react"; +import { useState } from "react"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; +} + +export function DataTable({ columns, data }: DataTableProps) { + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); + const [columnVisibility, setColumnVisibility] = useState({}); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setColumnFilters, + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + state: { + sorting, + columnFilters, + columnVisibility, + }, + }); + + return ( +
+
+ table.getColumn("email")?.setFilterValue(event.target.value)} + className="max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + column.toggleVisibility(!!value)} + > + {column.id} + + ); + })} + + +
+ +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} + + )) + ) : ( + + + Keine Ergebnisse gefunden. + + + )} + +
+
+
+ + +
+
+ ); +} + +================ +File: src/app/admin/users/dialog.tsx +================ +import type { User } from "@/app/admin/users/columns"; +import { ProfileForm } from "@/app/admin/users/form"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { PencilIcon } from "lucide-react"; + +interface EditUserDialogRootProps { + children: React.ReactNode; +} + +export function EditUserDialogRoot(props: EditUserDialogRootProps) { + const { children } = props; + + return {children}; +} + +export function EditUserDialogTrigger() { + return ( + + + Benutzer bearbeiten + + ); +} + +interface EditUserDialogContentProps { + user: User; +} + +export function EditUserDialogContent(props: EditUserDialogContentProps) { + const { user } = props; + + if (!user) { + return; + } + + return ( + + + Benutzer bearbeiten + + Hinweis: In den seltensten Fällen sollten die Daten + eines Benutzers geändert werden. Dies kann zu unerwarteten Problemen + führen. + + + + + ); +} + +================ +File: src/app/admin/users/form.tsx +================ +"use client"; + +import type { User } from "@/app/admin/users/columns"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { DialogClose } from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useToast } from "@/components/ui/use-toast"; +import { deleteUser, updateUser } from "@/server/actions/users"; +import type { UserRole } from "@/server/auth/permissions"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { SaveIcon, TrashIcon } from "lucide-react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +export const formSchema = z.object({ + username: z + .string() + .min(2, { + message: "Der Benutzername muss mindestens 2 Zeichen lang sein.", + }) + .max(50), + displayName: z + .string() + .min(2, { + message: "Der Anzeigename muss mindestens 2 Zeichen lang sein.", + }) + .max(50), + email: z.string().email(), + role: z.enum(["admin", "user", "guest"]), +}); + +interface ProfileFormProps { + user: User; +} + +export function ProfileForm(props: ProfileFormProps) { + const { user } = props; + const { toast } = useToast(); + + // 1. Define your form. + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + username: user.username, + displayName: user.displayName, + email: user.email, + role: user.role as UserRole, + }, + }); + + // 2. Define a submit handler. + async function onSubmit(values: z.infer) { + toast({ description: "Benutzerprofil wird aktualisiert..." }); + + await updateUser(user.id, values); + + toast({ description: "Benutzerprofil wurde aktualisiert." }); + } + + return ( +
+ + ( + + Benutzername + + + + + Nur in Ausnahmefällen sollte der Benutzername geändert werden. + + + + )} + /> + ( + + Anzeigename + + + + + Der Anzeigename darf frei verändert werden. + + + + )} + /> + ( + + E-Mail Adresse + + + + + Nur in Ausnahmefällen sollte die E-Mail Adresse geändert werden. + + + + )} + /> + ( + + Benutzerrolle + + + Die Benutzerrolle bestimmt die Berechtigungen des Benutzers. + + + + )} + /> +
+ + + + + + + Bist du dir sicher? + + Diese Aktion kann nicht rückgängig gemacht werden. Das + Benutzerprofil und die damit verbundenen Daten werden + unwiderruflich gelöscht. + + + + Abbrechen + { + toast({ description: "Benutzerprofil wird gelöscht..." }); + deleteUser(user.id); + toast({ description: "Benutzerprofil wurde gelöscht." }); + }} + > + Ja, löschen + + + + + + + +
+ + + ); +} + +================ +File: src/app/admin/users/page.tsx +================ +import { columns } from "@/app/admin/users/columns"; +import { DataTable } from "@/app/admin/users/data-table"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { db } from "@/server/db"; + +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Alle Benutzer", +}; + +export default async function AdminPage() { + const data = await db.query.users.findMany(); + + return ( + + + Benutzerverwaltung + Suche, Bearbeite und Lösche Benutzer + + + + + + ); +} + +================ +File: src/app/api/job/[jobId]/remaining-time/route.ts +================ +import { db } from "@/server/db"; +import { printJobs } from "@/server/db/schema"; +import { eq } from "drizzle-orm"; + +export const dynamic = "force-dynamic"; + +interface RemainingTimeRouteProps { + params: { + jobId: string; + }; +} +export async function GET(request: Request, { params }: RemainingTimeRouteProps) { + // Trying to fix build error in container... + if (params.jobId === undefined) { + return Response.json({}); + } + + // Get the job details + const jobDetails = await db.query.printJobs.findFirst({ + where: eq(printJobs.id, params.jobId), + }); + + // Check if the job exists + if (!jobDetails) { + return Response.json({ + id: params.jobId, + error: "Job not found", + }); + } + + // Calculate the remaining time + const startAt = new Date(jobDetails.startAt).getTime(); + const endAt = startAt + jobDetails.durationInMinutes * 60 * 1000; + const remainingTime = Math.max(0, endAt - Date.now()); + + // Return the remaining time + return Response.json({ + id: params.jobId, + remainingTime, + }); +} + +================ +File: src/app/api/printers/route.ts +================ +import { getPrinters } from "@/server/actions/printers"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + const printers = await getPrinters(); + + return Response.json(printers); +} + +================ +File: src/app/auth/login/callback/route.ts +================ +import { lucia } from "@/server/auth"; +import { type GitHubUserResult, github } from "@/server/auth/oauth"; +import { db } from "@/server/db"; +import { users } from "@/server/db/schema"; +import { OAuth2RequestError } from "arctic"; +import { eq } from "drizzle-orm"; +import { generateIdFromEntropySize } from "lucia"; +import { cookies } from "next/headers"; + +export const dynamic = "force-dynamic"; + +interface GithubEmailResponse { + email: string; + primary: boolean; + verified: boolean; + visibility: string; +} + +export async function GET(request: Request): Promise { + const url = new URL(request.url); + const code = url.searchParams.get("code"); + const state = url.searchParams.get("state"); + const storedState = cookies().get("github_oauth_state")?.value ?? null; + if (!code || !state || !storedState || state !== storedState) { + return new Response( + JSON.stringify({ + status_text: "Something is wrong", + data: { code, state, storedState }, + }), + { + status: 400, + }, + ); + } + + try { + const tokens = await github.validateAuthorizationCode(code); + const githubUserResponse = await fetch("https://git.i.mercedes-benz.com/api/v3/user", { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + }); + const githubUser: GitHubUserResult = await githubUserResponse.json(); + + // Sometimes email can be null in the user query. + if (githubUser.email === null || githubUser.email === undefined) { + const githubEmailResponse = await fetch("https://git.i.mercedes-benz.com/api/v3/user/emails", { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + }); + const githubUserEmail: GithubEmailResponse[] = await githubEmailResponse.json(); + githubUser.email = githubUserEmail[0].email; + } + const existingUser = await db.query.users.findFirst({ + where: eq(users.github_id, githubUser.id), + }); + + if (existingUser) { + const session = await lucia.createSession(existingUser.id, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes); + return new Response(null, { + status: 302, + headers: { + Location: "/", + }, + }); + } + + const userId = generateIdFromEntropySize(10); // 16 characters long + + await db.insert(users).values({ + id: userId, + github_id: githubUser.id, + username: githubUser.login, + displayName: githubUser.name, + email: githubUser.email, + }); + + const session = await lucia.createSession(userId, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + cookies().set(sessionCookie.name, sessionCookie.value, { + ...sessionCookie.attributes, + secure: false, // Else cookie does not get set cause IT has not provided us an SSL certificate yet + }); + return new Response(null, { + status: 302, + headers: { + Location: "/", + }, + }); + } catch (e) { + // the specific error message depends on the provider + if (e instanceof OAuth2RequestError) { + // invalid code + return new Response( + JSON.stringify({ + status_text: "Invalid code", + error: JSON.stringify(e), + }), + { + status: 400, + }, + ); + } + return new Response(null, { + status: 500, + }); + } +} + +================ +File: src/app/auth/login/route.ts +================ +import { github } from "@/server/auth/oauth"; +import { generateState } from "arctic"; +import { cookies } from "next/headers"; + +export const dynamic = "force-dynamic"; + +export async function GET(): Promise { + const state = generateState(); + const url = await github.createAuthorizationURL(state, { + scopes: ["user"], + }); + const ONE_HOUR = 60 * 60; + + cookies().set("github_oauth_state", state, { + path: "/", + secure: false, //process.env.NODE_ENV === "production", -- can't be used until SSL certificate is provided by IT + httpOnly: true, + maxAge: ONE_HOUR, + sameSite: "lax", + }); + + return Response.redirect(url); +} + +================ +File: src/app/globals.css +================ +@tailwind base; +@tailwind components; +@tailwind utilities; + + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 221.2 83.2% 53.3%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 221.2 83.2% 53.3%; + --radius: 0.75rem; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 217.2 91.2% 59.8%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 224.3 76.3% 48%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +================ +File: src/app/job/[jobId]/cancel-form.tsx +================ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { useToast } from "@/components/ui/use-toast"; +import { abortPrintJob } from "@/server/actions/printJobs"; +import { TriangleAlertIcon } from "lucide-react"; +import { useState } from "react"; + +const formSchema = z.object({ + abortReason: z + .string() + .min(1, { + message: "Bitte gebe einen Grund für den Abbruch an.", + }) + .max(255, { + message: "Der Grund darf maximal 255 Zeichen lang sein.", + }), +}); + +interface CancelFormProps { + jobId: string; +} + +export function CancelForm(props: CancelFormProps) { + const { jobId } = props; + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + abortReason: "", + }, + }); + const { toast } = useToast(); + const [open, setOpen] = useState(false); + + async function onSubmit(values: z.infer) { + toast({ + description: "Druckauftrag wird abgebrochen...", + }); + try { + const result = await abortPrintJob(jobId, values.abortReason); + if (result?.error) { + toast({ + description: result.error, + variant: "destructive", + }); + } + setOpen(false); + toast({ + description: "Druckauftrag wurde abgebrochen.", + }); + } catch (error) { + if (error instanceof Error) { + toast({ + description: error.message, + variant: "destructive", + }); + } else { + toast({ + description: "Ein unbekannter Fehler ist aufgetreten.", + variant: "destructive", + }); + } + } + } + + return ( + + + + + + + Druckauftrag abbrechen? + + Du bist dabei, den Druckauftrag abzubrechen. Bitte beachte, dass ein abgebrochener Druckauftrag nicht wieder + aufgenommen werden kann und der Drucker sich automatisch abschaltet. + + +
+ + ( + + Grund für den Abbruch + + + + + Bitte teile uns den Grund für den Abbruch des Druckauftrags mit. Wenn der Drucker eine Fehlermeldung + anzeigt, gib bitte nur diese Fehlermeldung an. + + + + )} + /> +
+ + + + +
+ + +
+
+ ); +} + +================ +File: src/app/job/[jobId]/edit-comments.tsx +================ +"use client"; + +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { useToast } from "@/components/ui/use-toast"; +import { updatePrintComments } from "@/server/actions/printJobs"; +import { useDebouncedCallback } from "use-debounce"; + +interface EditCommentsProps { + defaultValue: string | null; + jobId: string; + disabled?: boolean; +} +export function EditComments(props: EditCommentsProps) { + const { defaultValue, jobId, disabled } = props; + const { toast } = useToast(); + + const debounced = useDebouncedCallback(async (value) => { + try { + const result = await updatePrintComments(jobId, value); + if (result?.error) { + toast({ + description: result.error, + variant: "destructive", + }); + } + toast({ + description: "Anmerkungen wurden gespeichert.", + }); + } catch (error) { + if (error instanceof Error) { + toast({ + description: error.message, + variant: "destructive", + }); + } else { + toast({ + description: "Ein unbekannter Fehler ist aufgetreten.", + variant: "destructive", + }); + } + } + }, 1000); + + return ( +
+ +