From ff7dbcb4e4e3ca32b0019b53c88e627cc2b34dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 12 Apr 2026 21:26:12 +0200 Subject: [PATCH] first commit --- .dockerignore | 11 + .env.example | 9 + .gitignore | 33 + README.md | 95 + backend/.env.example | 4 + backend/Dockerfile | 16 + backend/app/__init__.py | 0 backend/app/api/__init__.py | 14 + backend/app/api/deps.py | 40 + backend/app/api/routes/auth.py | 106 + backend/app/api/routes/backups.py | 125 + backend/app/api/routes/dashboard.py | 42 + backend/app/api/routes/health.py | 19 + backend/app/api/routes/logs.py | 32 + backend/app/api/routes/routers.py | 71 + backend/app/api/routes/settings.py | 75 + backend/app/api/routes/swos_beta.py | 33 + backend/app/core/config.py | 38 + backend/app/core/cron_utils.py | 88 + backend/app/core/security.py | 22 + backend/app/db/base.py | 7 + backend/app/db/session.py | 76 + backend/app/main.py | 30 + backend/app/models/backup.py | 19 + backend/app/models/log.py | 12 + backend/app/models/router.py | 28 + backend/app/models/settings.py | 26 + backend/app/models/user.py | 15 + backend/app/schemas/auth.py | 31 + backend/app/schemas/backup.py | 49 + backend/app/schemas/dashboard.py | 28 + backend/app/schemas/router.py | 60 + backend/app/schemas/settings.py | 85 + backend/app/schemas/swos_beta.py | 33 + backend/app/services/backup_service.py | 321 + backend/app/services/file_service.py | 38 + backend/app/services/log_service.py | 24 + backend/app/services/notification_service.py | 78 + backend/app/services/router_service.py | 140 + backend/app/services/scheduler.py | 249 + backend/app/services/settings_service.py | 32 + backend/app/services/swos_beta_service.py | 124 + backend/requirements.txt | 14 + backend/scripts/migrate_legacy_sqlite.py | 117 + backend/tests/test_auth.py | 35 + backend/tests/test_health.py | 10 + backend/tests/test_scheduler.py | 24 + backend/tests/test_swos_beta.py | 74 + docker-compose.yml | 29 + env.example | 9 + frontend/Dockerfile | 12 + frontend/angular.json | 37 + frontend/nginx/default.conf | 78 + frontend/package-lock.json | 12730 ++++++++++++++++ frontend/package.json | 38 + frontend/proxy.conf.json | 8 + frontend/src/app/app.component.html | 68 + frontend/src/app/app.component.ts | 146 + frontend/src/app/app.routes.ts | 29 + frontend/src/app/core/guards/auth.guard.ts | 14 + frontend/src/app/core/guards/guest.guard.ts | 15 + .../app/core/interceptors/auth.interceptor.ts | 7 + .../app/core/services/api-status.service.ts | 83 + frontend/src/app/core/services/api.service.ts | 9 + .../src/app/core/services/auth.service.ts | 88 + .../src/app/core/services/font.service.ts | 56 + .../src/app/core/services/language.service.ts | 68 + .../src/app/core/services/layout.service.ts | 22 + .../src/app/core/services/theme.service.ts | 25 + frontend/src/app/core/services/ui.service.ts | 81 + .../auth/change-password-page.component.html | 50 + .../auth/change-password-page.component.ts | 90 + .../features/auth/login-page.component.html | 29 + .../app/features/auth/login-page.component.ts | 49 + .../auth/register-page.component.html | 28 + .../features/auth/register-page.component.ts | 59 + .../dashboard/dashboard-page.component.html | 107 + .../dashboard/dashboard-page.component.ts | 413 + .../diff-configs-page.component.html | 143 + .../diff-configs-page.component.ts | 248 + .../features/files/files-page.component.html | 186 + .../features/files/files-page.component.ts | 439 + .../features/logs/logs-page.component.html | 25 + .../app/features/logs/logs-page.component.ts | 63 + .../routers/router-detail-page.component.html | 164 + .../routers/router-detail-page.component.ts | 286 + .../routers/routers-page.component.html | 88 + .../routers/routers-page.component.ts | 131 + .../settings/settings-page.component.html | 308 + .../settings/settings-page.component.ts | 490 + .../swos-beta/swos-beta-page.component.html | 80 + .../swos-beta/swos-beta-page.component.ts | 131 + .../shared/auth/auth-toolbar.component.html | 15 + .../app/shared/auth/auth-toolbar.component.ts | 31 + .../shared/layout/app-sidebar.component.html | 27 + .../shared/layout/app-sidebar.component.ts | 16 + .../shared/layout/app-topbar.component.html | 35 + .../app/shared/layout/app-topbar.component.ts | 45 + .../app/shared/ui/page-header.component.html | 10 + .../app/shared/ui/page-header.component.ts | 14 + .../app/shared/ui/section-card.component.html | 12 + .../app/shared/ui/section-card.component.ts | 14 + .../app/shared/ui/stat-card.component.html | 13 + .../src/app/shared/ui/stat-card.component.ts | 20 + frontend/src/assets/i18n/en.json | 513 + frontend/src/assets/i18n/es.json | 513 + frontend/src/assets/i18n/no.json | 513 + frontend/src/assets/i18n/pl.json | 513 + frontend/src/favicon.ico | 0 frontend/src/index.html | 22 + frontend/src/main.ts | 32 + frontend/src/styles.css | 4 + frontend/src/styles/auth.css | 165 + frontend/src/styles/dashboard.css | 187 + frontend/src/styles/layout.css | 195 + frontend/src/styles/pages.css | 3412 +++++ frontend/tsconfig.app.json | 9 + frontend/tsconfig.json | 21 + make_zip.py | 70 + patch_routeros.py | 1207 ++ run_integration.py | 100 + start_dev.sh | 95 + start_prod.sh | 15 + 123 files changed, 27749 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 backend/.env.example create mode 100644 backend/Dockerfile create mode 100644 backend/app/__init__.py create mode 100644 backend/app/api/__init__.py create mode 100644 backend/app/api/deps.py create mode 100644 backend/app/api/routes/auth.py create mode 100644 backend/app/api/routes/backups.py create mode 100644 backend/app/api/routes/dashboard.py create mode 100644 backend/app/api/routes/health.py create mode 100644 backend/app/api/routes/logs.py create mode 100644 backend/app/api/routes/routers.py create mode 100644 backend/app/api/routes/settings.py create mode 100644 backend/app/api/routes/swos_beta.py create mode 100644 backend/app/core/config.py create mode 100644 backend/app/core/cron_utils.py create mode 100644 backend/app/core/security.py create mode 100644 backend/app/db/base.py create mode 100644 backend/app/db/session.py create mode 100644 backend/app/main.py create mode 100644 backend/app/models/backup.py create mode 100644 backend/app/models/log.py create mode 100644 backend/app/models/router.py create mode 100644 backend/app/models/settings.py create mode 100644 backend/app/models/user.py create mode 100644 backend/app/schemas/auth.py create mode 100644 backend/app/schemas/backup.py create mode 100644 backend/app/schemas/dashboard.py create mode 100644 backend/app/schemas/router.py create mode 100644 backend/app/schemas/settings.py create mode 100644 backend/app/schemas/swos_beta.py create mode 100644 backend/app/services/backup_service.py create mode 100644 backend/app/services/file_service.py create mode 100644 backend/app/services/log_service.py create mode 100644 backend/app/services/notification_service.py create mode 100644 backend/app/services/router_service.py create mode 100644 backend/app/services/scheduler.py create mode 100644 backend/app/services/settings_service.py create mode 100644 backend/app/services/swos_beta_service.py create mode 100644 backend/requirements.txt create mode 100755 backend/scripts/migrate_legacy_sqlite.py create mode 100644 backend/tests/test_auth.py create mode 100644 backend/tests/test_health.py create mode 100644 backend/tests/test_scheduler.py create mode 100644 backend/tests/test_swos_beta.py create mode 100644 docker-compose.yml create mode 100644 env.example create mode 100644 frontend/Dockerfile create mode 100644 frontend/angular.json create mode 100644 frontend/nginx/default.conf create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/proxy.conf.json create mode 100644 frontend/src/app/app.component.html create mode 100644 frontend/src/app/app.component.ts create mode 100644 frontend/src/app/app.routes.ts create mode 100644 frontend/src/app/core/guards/auth.guard.ts create mode 100644 frontend/src/app/core/guards/guest.guard.ts create mode 100644 frontend/src/app/core/interceptors/auth.interceptor.ts create mode 100644 frontend/src/app/core/services/api-status.service.ts create mode 100644 frontend/src/app/core/services/api.service.ts create mode 100644 frontend/src/app/core/services/auth.service.ts create mode 100644 frontend/src/app/core/services/font.service.ts create mode 100644 frontend/src/app/core/services/language.service.ts create mode 100644 frontend/src/app/core/services/layout.service.ts create mode 100644 frontend/src/app/core/services/theme.service.ts create mode 100644 frontend/src/app/core/services/ui.service.ts create mode 100644 frontend/src/app/features/auth/change-password-page.component.html create mode 100644 frontend/src/app/features/auth/change-password-page.component.ts create mode 100644 frontend/src/app/features/auth/login-page.component.html create mode 100644 frontend/src/app/features/auth/login-page.component.ts create mode 100644 frontend/src/app/features/auth/register-page.component.html create mode 100644 frontend/src/app/features/auth/register-page.component.ts create mode 100644 frontend/src/app/features/dashboard/dashboard-page.component.html create mode 100644 frontend/src/app/features/dashboard/dashboard-page.component.ts create mode 100644 frontend/src/app/features/diff-configs/diff-configs-page.component.html create mode 100644 frontend/src/app/features/diff-configs/diff-configs-page.component.ts create mode 100644 frontend/src/app/features/files/files-page.component.html create mode 100644 frontend/src/app/features/files/files-page.component.ts create mode 100644 frontend/src/app/features/logs/logs-page.component.html create mode 100644 frontend/src/app/features/logs/logs-page.component.ts create mode 100644 frontend/src/app/features/routers/router-detail-page.component.html create mode 100644 frontend/src/app/features/routers/router-detail-page.component.ts create mode 100644 frontend/src/app/features/routers/routers-page.component.html create mode 100644 frontend/src/app/features/routers/routers-page.component.ts create mode 100644 frontend/src/app/features/settings/settings-page.component.html create mode 100644 frontend/src/app/features/settings/settings-page.component.ts create mode 100644 frontend/src/app/features/swos-beta/swos-beta-page.component.html create mode 100644 frontend/src/app/features/swos-beta/swos-beta-page.component.ts create mode 100644 frontend/src/app/shared/auth/auth-toolbar.component.html create mode 100644 frontend/src/app/shared/auth/auth-toolbar.component.ts create mode 100644 frontend/src/app/shared/layout/app-sidebar.component.html create mode 100644 frontend/src/app/shared/layout/app-sidebar.component.ts create mode 100644 frontend/src/app/shared/layout/app-topbar.component.html create mode 100644 frontend/src/app/shared/layout/app-topbar.component.ts create mode 100644 frontend/src/app/shared/ui/page-header.component.html create mode 100644 frontend/src/app/shared/ui/page-header.component.ts create mode 100644 frontend/src/app/shared/ui/section-card.component.html create mode 100644 frontend/src/app/shared/ui/section-card.component.ts create mode 100644 frontend/src/app/shared/ui/stat-card.component.html create mode 100644 frontend/src/app/shared/ui/stat-card.component.ts create mode 100644 frontend/src/assets/i18n/en.json create mode 100644 frontend/src/assets/i18n/es.json create mode 100644 frontend/src/assets/i18n/no.json create mode 100644 frontend/src/assets/i18n/pl.json create mode 100644 frontend/src/favicon.ico create mode 100644 frontend/src/index.html create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/styles.css create mode 100644 frontend/src/styles/auth.css create mode 100644 frontend/src/styles/dashboard.css create mode 100644 frontend/src/styles/layout.css create mode 100644 frontend/src/styles/pages.css create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 make_zip.py create mode 100644 patch_routeros.py create mode 100644 run_integration.py create mode 100755 start_dev.sh create mode 100755 start_prod.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..dbce0f0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.git +.gitignore +**/__pycache__ +**/.pytest_cache +**/.mypy_cache +**/.ruff_cache +**/.venv +**/node_modules +**/dist +**/.angular +.env diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6e10c2e --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +SECRET_KEY=change-me-before-production +ACCESS_TOKEN_EXPIRE_MINUTES=7899999 +ALLOW_REGISTRATION=true +DEFAULT_ADMIN_USERNAME=admin +DEFAULT_ADMIN_PASSWORD=admin +APP_PORT=5580 +API_PREFIX=/api +DATA_DIR=/app/storage +DATABASE_URL=sqlite:////app/storage/routeros_backup_next.db \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d1e910 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Python +__pycache__/ +*.py[cod] +*.so +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.venv/ +venv/ +.env +.env.* +!.env.example + +# Backend runtime +backend/storage/routeros_backup_next.db +backend/data/ +backend/storage/ +data/ +*.log + +# Node / Angular +frontend/node_modules/ +frontend/dist/ +frontend/.angular/ +frontend/.cache/ + +# OS / editors +.DS_Store +.idea/ +.vscode/ + +*.zip +docker-data/* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..68a85c5 --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# RouterOS Backup Manager Next + +Refactor starej aplikacji Flask/Bootstrap do architektury: +- **backend:** FastAPI + SQLAlchemy + APScheduler +- **frontend:** Angular + PrimeNG + ngx-translate +- **dev:** lokalnie bez Dockera +- **prod:** Docker Compose + Nginx + FastAPI + +## Co poprawiono względem poprzedniej iteracji +- usunięte bezpośrednie wywołanie `uvicorn` ze skryptów startowych +- zależności backendu zaktualizowane pod nowszy FastAPI/Pydantic i Python 3.14 +- dev frontend domyślnie startuje na `127.0.0.1`, więc znika ostrzeżenie o otwartym `0.0.0.0` +- dodane środowisko produkcyjne: `Dockerfile`, `docker-compose.yml`, nginx proxy +- dodane `.env.example`, `.gitignore`, `.dockerignore`, `start_prod.sh` +- dodane brakujące ekrany: rejestracja, zmiana hasła +- frontend przebudowany wizualnie w kierunku **Avalon-inspired PrimeNG admin shell** +- wydzielone wspólne komponenty UI: `app-topbar`, `app-sidebar`, `app-page-header`, `app-stat-card`, `app-section-card` +- dodane brakujące operacje UI: edycja/usuwanie routera, export-all, binary-all, filtry i sortowanie plików +- dodany HTML diff side-by-side +- dodany migrator starej bazy SQLite +- przywrócona automatyczna retencja logów + +## Struktura +- `backend/` – FastAPI API +- `frontend/` – Angular UI +- `backend/scripts/migrate_legacy_sqlite.py` – migracja danych ze starej SQLite +- `start_dev.sh` – start lokalny bez Dockera +- `start_prod.sh` – start produkcyjny przez Docker Compose +- `.env.example` – konfiguracja Docker/produkcyjna +- `backend/.env.dev.example` – konfiguracja lokalna dla deweloperki bez Dockera +- `FEATURE_AUDIT.md` – porównanie starej i nowej wersji funkcjonalnie + +## Dev bez Dockera +Wymagania: +- Python 3.13 lub 3.14 +- Node.js 22+ +- npm + +Start: +```bash +cp backend/.env.dev.example backend/.env +./start_dev.sh +``` + +Adresy: +- backend: `http://127.0.0.1:8000` +- docs: `http://127.0.0.1:8000/docs` +- frontend: `http://127.0.0.1:4200` + +Dla wystawienia UI w LAN użyj: +```bash +cd frontend +npm run start:lan +``` + +Domyślne konto po pierwszym starcie: +- login: `admin` +- hasło: `admin` + +## Produkcja w Dockerze +```bash +cp .env.example .env +# uzupełnij SECRET_KEY i DEFAULT_ADMIN_PASSWORD +./start_prod.sh +``` + +Domyślnie frontend będzie dostępny na: +- `http://127.0.0.1:8080` + +## Konfiguracja środowisk + +### Docker / produkcja (`.env` w katalogu głównym) +Najważniejsze zmienne: +- `SECRET_KEY` +- `DATABASE_URL` +- `DATA_DIR` +- `ALLOW_REGISTRATION` +- `DEFAULT_ADMIN_USERNAME` +- `DEFAULT_ADMIN_PASSWORD` +- `CORS_ORIGINS` +- `FRONTEND_PORT` + +## Migracja starej bazy Flask/SQLite +Jeżeli masz starą bazę `backup_routeros.db`, możesz zaimportować dane: +```bash +cd backend +PYTHONPATH=. python scripts/migrate_legacy_sqlite.py /sciezka/do/backup_routeros.db +``` + + +### Dev bez Dockera (`backend/.env`) +Lokalny backend korzysta z `backend/.env`. Najprościej zacząć od: +```bash +cp backend/.env.dev.example backend/.env +``` diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..003f07a --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,4 @@ +APP_PORT=8080 +API_PREFIX=/api +DATA_DIR=/app/storage +DATABASE_URL=sqlite:////app/storage/routeros_backup_next.db \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..e3e416b --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.13-slim + +ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends curl build-essential && rm -rf /var/lib/apt/lists/* + +COPY backend/requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r /app/requirements.txt + +COPY backend /app +RUN mkdir -p /app/storage + +EXPOSE 8000 +CMD ["fastapi", "run", "app/main.py", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..a8b38f6 --- /dev/null +++ b/backend/app/api/__init__.py @@ -0,0 +1,14 @@ +from fastapi import APIRouter + +from app.api.routes import auth, backups, dashboard, health, logs, routers, settings, swos_beta + +api_router = APIRouter() +api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) +api_router.include_router(dashboard.router, prefix="/dashboard", tags=["dashboard"]) +api_router.include_router(routers.router, prefix="/routers", tags=["routers"]) +api_router.include_router(backups.router, prefix="/backups", tags=["backups"]) +api_router.include_router(settings.router, prefix="/settings", tags=["settings"]) +api_router.include_router(logs.router, prefix="/logs", tags=["logs"]) +api_router.include_router(health.router, tags=["health"]) + +api_router.include_router(swos_beta.router, prefix='/swos-beta', tags=['swos-beta']) diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py new file mode 100644 index 0000000..694e674 --- /dev/null +++ b/backend/app/api/deps.py @@ -0,0 +1,40 @@ +from typing import Generator + +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from jose import JWTError, jwt +from sqlalchemy.orm import Session + +from app.core.config import settings +from app.db.session import SessionLocal +from app.models.user import User + + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") + + +def get_db() -> Generator[Session, None, None]: + db = SessionLocal() + try: + yield db + finally: + db.close() + + +def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> User: + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, settings.secret_key, algorithms=[settings.jwt_algorithm]) + username: str | None = payload.get("sub") + if username is None: + raise credentials_exception + except JWTError as exc: + raise credentials_exception from exc + user = db.query(User).filter(User.username == username).first() + if not user: + raise credentials_exception + return user diff --git a/backend/app/api/routes/auth.py b/backend/app/api/routes/auth.py new file mode 100644 index 0000000..2b46804 --- /dev/null +++ b/backend/app/api/routes/auth.py @@ -0,0 +1,106 @@ +from datetime import timedelta + +from fastapi import APIRouter, Depends, HTTPException, Request, status +from sqlalchemy.orm import Session + +from app.api.deps import get_current_user, get_db +from app.core.config import settings +from app.core.security import create_access_token, get_password_hash, verify_password +from app.models.user import User +from app.schemas.auth import ( + ChangePasswordRequest, + RegisterRequest, + TokenResponse, + UpdateUserPreferencesRequest, + UserResponse, +) + +router = APIRouter() + + +@router.post("/register", response_model=UserResponse) +def register(payload: RegisterRequest, db: Session = Depends(get_db)): + if not settings.allow_registration: + raise HTTPException(status_code=403, detail="Registration is disabled") + existing = db.query(User).filter(User.username == payload.username).first() + if existing: + raise HTTPException(status_code=409, detail="Username already exists") + user = User(username=payload.username, password_hash=get_password_hash(payload.password)) + db.add(user) + db.commit() + db.refresh(user) + return user + + +@router.post("/login", response_model=TokenResponse) +async def login(request: Request, db: Session = Depends(get_db)): + username = None + password = None + content_type = (request.headers.get("content-type") or "").lower() + + if "application/json" in content_type: + try: + payload = await request.json() + except Exception: + payload = {} + username = payload.get("username") + password = payload.get("password") + else: + form_data = await request.form() + username = form_data.get("username") + password = form_data.get("password") + + if not username or not password: + raise HTTPException(status_code=422, detail="Username and password are required") + + user = db.query(User).filter(User.username == username).first() + if not user or not verify_password(password, user.password_hash): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") + token = create_access_token( + subject=user.username, + expires_delta=timedelta(minutes=settings.access_token_expire_minutes), + ) + return TokenResponse(access_token=token, user=UserResponse.model_validate(user)) + + +@router.get("/me", response_model=UserResponse) +def me(current_user: User = Depends(get_current_user)): + return current_user + + + + +@router.put("/preferences", response_model=UserResponse) +def update_preferences( + payload: UpdateUserPreferencesRequest, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + preferred_language = (payload.preferred_language or 'pl').strip().lower() + preferred_font = (payload.preferred_font or 'default').strip().lower() + + if preferred_language not in {'pl', 'en', 'es', 'no'}: + raise HTTPException(status_code=422, detail='Unsupported language') + if preferred_font not in {'default', 'adwaita_mono', 'hack'}: + raise HTTPException(status_code=422, detail='Unsupported font') + + current_user.preferred_language = preferred_language + current_user.preferred_font = preferred_font + db.add(current_user) + db.commit() + db.refresh(current_user) + return current_user + + +@router.post("/change-password") +def change_password( + payload: ChangePasswordRequest, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + if not verify_password(payload.current_password, current_user.password_hash): + raise HTTPException(status_code=400, detail="Current password is invalid") + current_user.password_hash = get_password_hash(payload.new_password) + db.add(current_user) + db.commit() + return {"message": "Password changed successfully"} diff --git a/backend/app/api/routes/backups.py b/backend/app/api/routes/backups.py new file mode 100644 index 0000000..16e6a02 --- /dev/null +++ b/backend/app/api/routes/backups.py @@ -0,0 +1,125 @@ +import io +import zipfile + +from fastapi import APIRouter, Depends, HTTPException, Query +from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse +from sqlalchemy.orm import Session + +from app.api.deps import get_current_user, get_db +from app.models.user import User +from app.schemas.backup import BackupDiffResponse, BackupResponse, BulkActionRequest +from app.services.backup_service import backup_service + +router = APIRouter() + + +@router.get("", response_model=list[BackupResponse]) +def list_backups( + search: str | None = Query(default=None), + backup_type: str | None = Query(default=None, pattern="^(export|binary)$"), + router_id: int | None = Query(default=None), + sort_by: str = Query(default="created_at"), + order: str = Query(default="desc", pattern="^(asc|desc)$"), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + return backup_service.list_backups( + db, + current_user, + search=search, + backup_type=backup_type, + router_id=router_id, + sort_by=sort_by, + order=order, + ) + + +@router.get("/router/{router_id}", response_model=list[BackupResponse]) +def list_router_backups(router_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + return backup_service.list_router_backups(db, current_user, router_id) + + +@router.post("/routers/export-all") +def export_all(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + return backup_service.export_all(db, current_user) + + +@router.post("/routers/binary-all") +def binary_all(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + return backup_service.binary_all(db, current_user) + + +@router.post("/router/{router_id}/export", response_model=BackupResponse) +def export_router(router_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + return backup_service.export_router(db, current_user, router_id) + + +@router.post("/router/{router_id}/binary", response_model=BackupResponse) +def binary_backup(router_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + return backup_service.binary_backup(db, current_user, router_id) + + +@router.post("/router/{router_id}/upload/{backup_id}") +def upload_to_router(router_id: int, backup_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + backup_service.upload_backup_to_router(db, current_user, router_id, backup_id) + return {"message": "Backup uploaded to router"} + + +@router.delete("/{backup_id}") +def delete_backup(backup_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + backup_service.delete_backup(db, current_user, backup_id) + return {"message": "Backup deleted"} + + +@router.get("/{backup_id}/download") +def download_backup(backup_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + backup = backup_service.get_backup_for_user(db, current_user, backup_id) + return FileResponse(path=backup.file_path, filename=backup.file_name) + + +@router.get("/{backup_id}/view") +def view_export(backup_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + backup = backup_service.get_backup_for_user(db, current_user, backup_id) + if backup.backup_type != "export": + raise HTTPException(status_code=400, detail="Only export backups can be viewed") + with open(backup.file_path, "r", encoding="utf-8", errors="ignore") as handle: + return {"content": handle.read(), "backup": BackupResponse.model_validate(backup_service._serialize_backup(backup))} + + +@router.post("/{backup_id}/email") +def email_backup(backup_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + backup_service.email_backup(db, current_user, backup_id) + return {"message": "Backup sent by email"} + + +@router.get("/{left_id}/diff/{right_id}", response_model=BackupDiffResponse) +def diff_backups(left_id: int, right_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + return backup_service.diff_backups(db, current_user, left_id, right_id) + + +@router.get("/{left_id}/diff/{right_id}/html", response_class=HTMLResponse) +def diff_backups_html(left_id: int, right_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + result = backup_service.diff_backups(db, current_user, left_id, right_id) + return HTMLResponse(result["diff_html"]) + + +@router.post("/bulk") +def bulk_action(payload: BulkActionRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + backups = [backup_service.get_backup_for_user(db, current_user, backup_id) for backup_id in payload.backup_ids] + if payload.action == "delete": + for backup in backups: + backup_service.delete_backup(db, current_user, backup.id, commit=False) + db.commit() + return {"message": f"Deleted {len(backups)} backups"} + if payload.action == "download": + stream = io.BytesIO() + with zipfile.ZipFile(stream, "w") as archive: + for backup in backups: + archive.write(backup.file_path, backup.file_name) + stream.seek(0) + return StreamingResponse( + stream, + media_type="application/zip", + headers={"Content-Disposition": 'attachment; filename="backups.zip"'}, + ) + raise HTTPException(status_code=400, detail="Unsupported bulk action") diff --git a/backend/app/api/routes/dashboard.py b/backend/app/api/routes/dashboard.py new file mode 100644 index 0000000..d16fb2e --- /dev/null +++ b/backend/app/api/routes/dashboard.py @@ -0,0 +1,42 @@ +from fastapi import APIRouter, Depends +from sqlalchemy import func +from sqlalchemy.orm import Session + +from app.api.deps import get_current_user, get_db +from app.models.backup import Backup +from app.models.log import OperationLog +from app.models.router import Router +from app.models.user import User +from app.schemas.dashboard import DashboardResponse +from app.services.file_service import get_storage_stats + +router = APIRouter() + + +@router.get("", response_model=DashboardResponse) +def get_dashboard(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + routers_count = db.query(func.count(Router.id)).filter(Router.owner_id == current_user.id).scalar() or 0 + export_count = ( + db.query(func.count(Backup.id)) + .join(Router) + .filter(Router.owner_id == current_user.id, Backup.backup_type == "export") + .scalar() + or 0 + ) + binary_count = ( + db.query(func.count(Backup.id)) + .join(Router) + .filter(Router.owner_id == current_user.id, Backup.backup_type == "binary") + .scalar() + or 0 + ) + recent_logs = db.query(OperationLog).order_by(OperationLog.timestamp.desc()).limit(10).all() + storage = get_storage_stats() + return DashboardResponse( + routers_count=routers_count, + export_count=export_count, + binary_count=binary_count, + total_backups=export_count + binary_count, + storage=storage, + recent_logs=recent_logs, + ) diff --git a/backend/app/api/routes/health.py b/backend/app/api/routes/health.py new file mode 100644 index 0000000..4390514 --- /dev/null +++ b/backend/app/api/routes/health.py @@ -0,0 +1,19 @@ +from datetime import datetime, timezone + +from fastapi import APIRouter, Depends +from sqlalchemy import text +from sqlalchemy.orm import Session + +from app.api.deps import get_db + +router = APIRouter() + + +@router.get("/health") +def health(db: Session = Depends(get_db)): + status = "ok" + try: + db.execute(text("SELECT 1")) + except Exception: + status = "error" + return {"status": status, "timestamp": datetime.now(timezone.utc).isoformat()} diff --git a/backend/app/api/routes/logs.py b/backend/app/api/routes/logs.py new file mode 100644 index 0000000..4814620 --- /dev/null +++ b/backend/app/api/routes/logs.py @@ -0,0 +1,32 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from app.api.deps import get_current_user, get_db +from app.models.log import OperationLog +from app.models.user import User +from app.services.log_service import log_service + +router = APIRouter() + + +@router.get("") +def get_logs( + limit: int = Query(100, ge=1, le=500), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + _ = current_user + return db.query(OperationLog).order_by(OperationLog.timestamp.desc()).limit(limit).all() + + +@router.delete("/older-than/{days}") +def delete_logs( + days: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + _ = current_user + if days < 1: + raise HTTPException(status_code=400, detail="Days must be >= 1") + deleted = log_service.delete_older_than(db, days) + return {"message": f"Deleted {deleted} logs", "deleted": deleted} diff --git a/backend/app/api/routes/routers.py b/backend/app/api/routes/routers.py new file mode 100644 index 0000000..ecdb863 --- /dev/null +++ b/backend/app/api/routes/routers.py @@ -0,0 +1,71 @@ +from pathlib import Path + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session + +from app.api.deps import get_current_user, get_db +from app.models.router import Router +from app.models.user import User +from app.schemas.router import RouterCreate, RouterResponse, RouterTestConnection, RouterUpdate +from app.services.router_service import router_service +from app.services.settings_service import settings_service + +router = APIRouter() + + +@router.get("", response_model=list[RouterResponse]) +def list_routers(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + return db.query(Router).filter(Router.owner_id == current_user.id).order_by(Router.created_at.desc()).all() + + +@router.post("", response_model=RouterResponse) +def create_router(payload: RouterCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + router = Router(**payload.model_dump(), owner_id=current_user.id) + db.add(router) + db.commit() + db.refresh(router) + return router + + +@router.get("/{router_id}", response_model=RouterResponse) +def get_router(router_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + router = db.query(Router).filter(Router.id == router_id, Router.owner_id == current_user.id).first() + if not router: + raise HTTPException(status_code=404, detail="Router not found") + return router + + +@router.put("/{router_id}", response_model=RouterResponse) +def update_router(router_id: int, payload: RouterUpdate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + router = db.query(Router).filter(Router.id == router_id, Router.owner_id == current_user.id).first() + if not router: + raise HTTPException(status_code=404, detail="Router not found") + for key, value in payload.model_dump(exclude_unset=True).items(): + setattr(router, key, value) + db.add(router) + db.commit() + db.refresh(router) + return router + + +@router.delete("/{router_id}") +def delete_router(router_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + router = db.query(Router).filter(Router.id == router_id, Router.owner_id == current_user.id).first() + if not router: + raise HTTPException(status_code=404, detail="Router not found") + for backup in list(router.backups): + path = Path(backup.file_path) + if path.exists(): + path.unlink() + db.delete(router) + db.commit() + return {"message": "Router deleted"} + + +@router.get("/{router_id}/test-connection", response_model=RouterTestConnection) +def test_connection(router_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + router = db.query(Router).filter(Router.id == router_id, Router.owner_id == current_user.id).first() + if not router: + raise HTTPException(status_code=404, detail="Router not found") + settings = settings_service.get_or_create(db) + return router_service.test_connection(db, router, settings.global_ssh_key) diff --git a/backend/app/api/routes/settings.py b/backend/app/api/routes/settings.py new file mode 100644 index 0000000..99ca8a3 --- /dev/null +++ b/backend/app/api/routes/settings.py @@ -0,0 +1,75 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session + +from app.api.deps import get_current_user, get_db +from app.core.security import verify_password +from app.models.settings import GlobalSettings +from app.models.user import User +from app.schemas.settings import ( + RevealSshKeyRequest, + RevealSshKeyResponse, + SchedulerStatusResponse, + SettingsResponse, + SettingsUpdate, +) +from app.services.notification_service import notification_service +from app.services.scheduler import scheduler_service +from app.services.settings_service import settings_service + +router = APIRouter() + + +def serialize_settings(settings: GlobalSettings) -> SettingsResponse: + payload = SettingsResponse.model_validate(settings, from_attributes=True).model_dump() + payload['global_ssh_key'] = None + payload['has_global_ssh_key'] = bool((settings.global_ssh_key or '').strip()) + return SettingsResponse.model_validate(payload) + + +@router.get('', response_model=SettingsResponse) +def get_settings(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + _ = current_user + settings = settings_service.get_or_create(db) + return serialize_settings(settings) + + +@router.get('/scheduler-status', response_model=SchedulerStatusResponse) +def get_scheduler_status(current_user: User = Depends(get_current_user)): + _ = current_user + return scheduler_service.scheduler_status() + + +@router.put('', response_model=SettingsResponse) +def update_settings(payload: SettingsUpdate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + _ = current_user + settings = settings_service.update(db, payload) + scheduler_service.reschedule() + return serialize_settings(settings) + + +@router.post('/reveal-ssh-key', response_model=RevealSshKeyResponse) +def reveal_ssh_key( + payload: RevealSshKeyRequest, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + if not verify_password(payload.password, current_user.password_hash): + raise HTTPException(status_code=400, detail='Current password is invalid') + settings = settings_service.get_or_create(db) + return {'global_ssh_key': settings.global_ssh_key} + + +@router.post('/test-email') +def test_email(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + _ = current_user + settings = settings_service.get_or_create(db) + notification_service.send_test_email(settings) + return {'message': 'Test email sent'} + + +@router.post('/test-pushover') +def test_pushover(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + _ = current_user + settings = settings_service.get_or_create(db) + notification_service.send_test_pushover(settings) + return {'message': 'Test pushover sent'} diff --git a/backend/app/api/routes/swos_beta.py b/backend/app/api/routes/swos_beta.py new file mode 100644 index 0000000..71f3565 --- /dev/null +++ b/backend/app/api/routes/swos_beta.py @@ -0,0 +1,33 @@ +from fastapi import APIRouter, Depends, HTTPException +from fastapi.responses import StreamingResponse + +from app.api.deps import get_current_user +from app.models.user import User +from app.schemas.swos_beta import SwosBetaCredentials, SwosBetaProbeResponse +from app.services.swos_beta_service import swos_beta_service + +router = APIRouter() + + +@router.post('/probe', response_model=SwosBetaProbeResponse) +def probe_swos(payload: SwosBetaCredentials, current_user: User = Depends(get_current_user)): + del current_user + try: + return swos_beta_service.probe(payload) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + +@router.post('/download') +def download_swos_backup(payload: SwosBetaCredentials, current_user: User = Depends(get_current_user)): + del current_user + try: + backup = swos_beta_service.download_backup(payload) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + return StreamingResponse( + iter([backup.content]), + media_type=backup.content_type, + headers={'Content-Disposition': f'attachment; filename="{backup.filename}"'}, + ) diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..9b4fa67 --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,38 @@ +from pathlib import Path + +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + app_name: str = 'RouterOS Backup Manager Next' + app_env: str = 'development' + secret_key: str = 'change-me' + jwt_algorithm: str = 'HS256' + access_token_expire_minutes: int = 1440 + database_url: str = 'sqlite:///./storage/routeros_backup_next.db' + data_dir: str = './storage' + allow_registration: bool = True + api_prefix: str = '/api' + timezone: str = 'Europe/Warsaw' + default_admin_username: str = 'admin' + default_admin_password: str = 'admin' + smtp_starttls: bool = True + smtp_timeout_seconds: int = 20 + cors_origins: list[str] = Field(default_factory=lambda: ['http://localhost:4200', 'http://127.0.0.1:4200']) + + model_config = SettingsConfigDict( + env_file='.env', + env_file_encoding='utf-8', + extra='ignore', + env_nested_delimiter='__', + ) + + @property + def data_path(self) -> Path: + path = Path(self.data_dir) + path.mkdir(parents=True, exist_ok=True) + return path + + +settings = Settings() diff --git a/backend/app/core/cron_utils.py b/backend/app/core/cron_utils.py new file mode 100644 index 0000000..c12872e --- /dev/null +++ b/backend/app/core/cron_utils.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from datetime import datetime +from zoneinfo import ZoneInfo + +from apscheduler.triggers.cron import CronTrigger + +WEEKDAY_LABELS = { + '0': 'Sunday', + '1': 'Monday', + '2': 'Tuesday', + '3': 'Wednesday', + '4': 'Thursday', + '5': 'Friday', + '6': 'Saturday', + '7': 'Sunday', + 'sun': 'Sunday', + 'mon': 'Monday', + 'tue': 'Tuesday', + 'wed': 'Wednesday', + 'thu': 'Thursday', + 'fri': 'Friday', + 'sat': 'Saturday', +} + + +class CronValidationError(ValueError): + pass + + +def parse_cron_expression(expr: str, timezone_str: str) -> CronTrigger: + expr = (expr or '').strip() + if not expr: + raise CronValidationError('Cron expression cannot be empty') + + parts = expr.split() + if len(parts) != 5: + raise CronValidationError('Cron expression must contain exactly 5 fields') + + minute, hour, day, month, day_of_week = parts + try: + return CronTrigger( + minute=minute, + hour=hour, + day=day, + month=month, + day_of_week=day_of_week, + timezone=ZoneInfo(timezone_str), + ) + except Exception as exc: # pragma: no cover - APScheduler formats messages + raise CronValidationError(str(exc)) from exc + + +def validate_cron_expression(expr: str, timezone_str: str) -> None: + parse_cron_expression(expr, timezone_str) + + +def preview_next_runs(expr: str, timezone_str: str, count: int = 3) -> list[datetime]: + trigger = parse_cron_expression(expr, timezone_str) + now = datetime.now(ZoneInfo(timezone_str)) + previous = None + runs: list[datetime] = [] + for _ in range(max(count, 0)): + next_run = trigger.get_next_fire_time(previous, now) + if not next_run: + break + runs.append(next_run) + previous = next_run + now = next_run + return runs + + +def describe_cron_expression(expr: str) -> str: + expr = (expr or '').strip() + if not expr: + return 'Disabled' + + parts = expr.split() + if len(parts) != 5: + return 'Custom cron' + + minute, hour, day, month, day_of_week = parts + if minute.isdigit() and hour.isdigit() and day == '*' and month == '*' and day_of_week == '*': + return f'Every day at {int(hour):02d}:{int(minute):02d}' + if minute.isdigit() and hour.isdigit() and day == '*' and month == '*' and day_of_week.lower() in WEEKDAY_LABELS: + weekday = WEEKDAY_LABELS[day_of_week.lower()] + return f'Every {weekday} at {int(hour):02d}:{int(minute):02d}' + return 'Custom cron' diff --git a/backend/app/core/security.py b/backend/app/core/security.py new file mode 100644 index 0000000..2846a6e --- /dev/null +++ b/backend/app/core/security.py @@ -0,0 +1,22 @@ +from datetime import datetime, timedelta, timezone + +from jose import jwt +from passlib.context import CryptContext + +from app.core.config import settings + +pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto") + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) + + +def create_access_token(subject: str, expires_delta: timedelta) -> str: + expire = datetime.now(timezone.utc) + expires_delta + to_encode = {"sub": subject, "exp": expire} + return jwt.encode(to_encode, settings.secret_key, algorithm=settings.jwt_algorithm) diff --git a/backend/app/db/base.py b/backend/app/db/base.py new file mode 100644 index 0000000..61c0929 --- /dev/null +++ b/backend/app/db/base.py @@ -0,0 +1,7 @@ +from app.models.backup import Backup +from app.models.log import OperationLog +from app.models.router import Router +from app.models.settings import GlobalSettings +from app.models.user import User + +__all__ = ["User", "Router", "Backup", "OperationLog", "GlobalSettings"] diff --git a/backend/app/db/session.py b/backend/app/db/session.py new file mode 100644 index 0000000..ea29271 --- /dev/null +++ b/backend/app/db/session.py @@ -0,0 +1,76 @@ +from pathlib import Path + +from sqlalchemy import create_engine, inspect, text +from sqlalchemy.orm import declarative_base, sessionmaker + +from app.core.config import settings +from app.core.security import get_password_hash + + +def _ensure_sqlite_parent(database_url: str) -> None: + if not database_url.startswith('sqlite:///'): + return + relative_path = database_url.removeprefix('sqlite:///') + if not relative_path or relative_path == ':memory:': + return + db_path = Path(relative_path) + if not db_path.is_absolute(): + db_path = Path.cwd() / db_path + db_path.parent.mkdir(parents=True, exist_ok=True) + + +_ensure_sqlite_parent(settings.database_url) + +engine = create_engine( + settings.database_url, + connect_args={'check_same_thread': False} if settings.database_url.startswith('sqlite') else {}, +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + + +def _ensure_column(table_name: str, column_name: str, ddl: str) -> None: + inspector = inspect(engine) + existing = {column['name'] for column in inspector.get_columns(table_name)} + if column_name in existing: + return + with engine.begin() as connection: + connection.execute(text(f'ALTER TABLE {table_name} ADD COLUMN {column_name} {ddl}')) + + +def _run_lightweight_migrations() -> None: + tables = set(inspect(engine).get_table_names()) + if 'global_settings' in tables: + _ensure_column('global_settings', 'connection_test_interval_minutes', 'INTEGER DEFAULT 0') + if 'users' in tables: + _ensure_column('users', 'preferred_language', "VARCHAR(8) DEFAULT 'pl' NOT NULL") + _ensure_column('users', 'preferred_font', "VARCHAR(32) DEFAULT 'default' NOT NULL") + if 'routers' in tables: + _ensure_column('routers', 'last_connection_status', 'BOOLEAN') + _ensure_column('routers', 'last_connection_tested_at', 'DATETIME') + _ensure_column('routers', 'last_connection_error', 'TEXT') + _ensure_column('routers', 'last_connection_hostname', 'VARCHAR(255)') + _ensure_column('routers', 'last_connection_model', 'VARCHAR(255)') + _ensure_column('routers', 'last_connection_version', 'VARCHAR(255)') + _ensure_column('routers', 'last_connection_uptime', 'VARCHAR(255)') + + +def init_db(): + import app.db.base # noqa: F401 + from app.models.settings import GlobalSettings + from app.models.user import User + + Base.metadata.create_all(bind=engine) + _run_lightweight_migrations() + with SessionLocal() as db: + if not db.query(User).first(): + db.add( + User( + username=settings.default_admin_username, + password_hash=get_password_hash(settings.default_admin_password), + ) + ) + db.commit() + if not db.query(GlobalSettings).first(): + db.add(GlobalSettings()) + db.commit() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..c060f9f --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,30 @@ +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.api import api_router +from app.core.config import settings +from app.db.session import init_db +from app.services.scheduler import scheduler_service + + +@asynccontextmanager +async def lifespan(app: FastAPI): + init_db() + scheduler_service.start() + try: + yield + finally: + scheduler_service.shutdown() + + +app = FastAPI(title=settings.app_name, version="1.1.0", lifespan=lifespan) +app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) +app.include_router(api_router, prefix=settings.api_prefix) diff --git a/backend/app/models/backup.py b/backend/app/models/backup.py new file mode 100644 index 0000000..7c1ccae --- /dev/null +++ b/backend/app/models/backup.py @@ -0,0 +1,19 @@ +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func + +from app.db.session import Base + + +class Backup(Base): + __tablename__ = "backups" + + id = Column(Integer, primary_key=True, index=True) + router_id = Column(Integer, ForeignKey("routers.id"), nullable=False, index=True) + file_path = Column(String(500), nullable=False) + file_name = Column(String(255), nullable=False) + backup_type = Column(String(50), nullable=False, default="export") + checksum = Column(String(64), nullable=True) + created_at = Column(DateTime, server_default=func.now(), nullable=False) + + router = relationship("Router", back_populates="backups") diff --git a/backend/app/models/log.py b/backend/app/models/log.py new file mode 100644 index 0000000..cd5ad69 --- /dev/null +++ b/backend/app/models/log.py @@ -0,0 +1,12 @@ +from sqlalchemy import Column, DateTime, Integer, Text +from sqlalchemy.sql import func + +from app.db.session import Base + + +class OperationLog(Base): + __tablename__ = "operation_logs" + + id = Column(Integer, primary_key=True, index=True) + message = Column(Text, nullable=False) + timestamp = Column(DateTime, server_default=func.now(), nullable=False) diff --git a/backend/app/models/router.py b/backend/app/models/router.py new file mode 100644 index 0000000..ab44dc2 --- /dev/null +++ b/backend/app/models/router.py @@ -0,0 +1,28 @@ +from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func + +from app.db.session import Base + + +class Router(Base): + __tablename__ = "routers" + + id = Column(Integer, primary_key=True, index=True) + owner_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) + name = Column(String(120), nullable=False) + host = Column(String(255), nullable=False) + port = Column(Integer, nullable=False, default=22) + ssh_user = Column(String(120), nullable=False, default="admin") + ssh_key = Column(Text, nullable=True) + ssh_password = Column(String(255), nullable=True) + last_connection_status = Column(Boolean, nullable=True) + last_connection_tested_at = Column(DateTime, nullable=True) + last_connection_error = Column(Text, nullable=True) + last_connection_hostname = Column(String(255), nullable=True) + last_connection_model = Column(String(255), nullable=True) + last_connection_version = Column(String(255), nullable=True) + last_connection_uptime = Column(String(255), nullable=True) + created_at = Column(DateTime, server_default=func.now(), nullable=False) + + backups = relationship("Backup", back_populates="router", cascade="all, delete-orphan") diff --git a/backend/app/models/settings.py b/backend/app/models/settings.py new file mode 100644 index 0000000..9a96dab --- /dev/null +++ b/backend/app/models/settings.py @@ -0,0 +1,26 @@ +from sqlalchemy import Boolean, Column, Integer, String, Text + +from app.db.session import Base + + +class GlobalSettings(Base): + __tablename__ = "global_settings" + + id = Column(Integer, primary_key=True) + backup_retention_days = Column(Integer, default=7) + log_retention_days = Column(Integer, default=7) + export_cron = Column(String(64), default="") + binary_cron = Column(String(64), default="") + retention_cron = Column(String(64), default="") + enable_auto_export = Column(Boolean, default=False) + connection_test_interval_minutes = Column(Integer, default=0) + global_ssh_key = Column(Text, nullable=True) + pushover_token = Column(String(255), nullable=True) + pushover_userkey = Column(String(255), nullable=True) + notify_failures_only = Column(Boolean, default=True) + smtp_host = Column(String(255), nullable=True) + smtp_port = Column(Integer, default=587) + smtp_login = Column(String(255), nullable=True) + smtp_password = Column(String(255), nullable=True) + smtp_notifications_enabled = Column(Boolean, default=False) + recipient_email = Column(String(255), nullable=True) diff --git a/backend/app/models/user.py b/backend/app/models/user.py new file mode 100644 index 0000000..b8206bd --- /dev/null +++ b/backend/app/models/user.py @@ -0,0 +1,15 @@ +from sqlalchemy import Column, DateTime, Integer, String +from sqlalchemy.sql import func + +from app.db.session import Base + + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String(120), unique=True, nullable=False, index=True) + password_hash = Column(String(255), nullable=False) + created_at = Column(DateTime, server_default=func.now(), nullable=False) + preferred_language = Column(String(8), nullable=False, default='pl') + preferred_font = Column(String(32), nullable=False, default='default') diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py new file mode 100644 index 0000000..0df53f5 --- /dev/null +++ b/backend/app/schemas/auth.py @@ -0,0 +1,31 @@ +from pydantic import BaseModel, Field + + +class UserResponse(BaseModel): + id: int + username: str + preferred_language: str = 'pl' + preferred_font: str = 'default' + + model_config = {"from_attributes": True} + + +class TokenResponse(BaseModel): + access_token: str + token_type: str = "bearer" + user: UserResponse + + +class RegisterRequest(BaseModel): + username: str = Field(min_length=3, max_length=120) + password: str = Field(min_length=4, max_length=128) + + +class ChangePasswordRequest(BaseModel): + current_password: str + new_password: str = Field(min_length=4, max_length=128) + + +class UpdateUserPreferencesRequest(BaseModel): + preferred_language: str = Field(default='pl', min_length=2, max_length=8) + preferred_font: str = Field(default='default', min_length=2, max_length=32) diff --git a/backend/app/schemas/backup.py b/backend/app/schemas/backup.py new file mode 100644 index 0000000..7240f75 --- /dev/null +++ b/backend/app/schemas/backup.py @@ -0,0 +1,49 @@ +from datetime import datetime +from typing import Literal + +from pydantic import BaseModel + + +class BackupResponse(BaseModel): + id: int + router_id: int + router_name: str | None = None + file_path: str + file_name: str + backup_type: str + checksum: str | None = None + file_size: int | None = None + created_at: datetime + + model_config = {'from_attributes': True} + + +class BackupDiffLine(BaseModel): + type: Literal['context', 'added', 'removed', 'modified'] + left_number: int | None = None + right_number: int | None = None + left_text: str = '' + right_text: str = '' + + +class BackupDiffStats(BaseModel): + added: int = 0 + removed: int = 0 + modified: int = 0 + context: int = 0 + + +class BackupDiffResponse(BaseModel): + left_backup_id: int + right_backup_id: int + left_file_name: str | None = None + right_file_name: str | None = None + diff_text: str + diff_html: str | None = None + stats: BackupDiffStats | None = None + lines: list[BackupDiffLine] = [] + + +class BulkActionRequest(BaseModel): + action: Literal['download', 'delete'] + backup_ids: list[int] diff --git a/backend/app/schemas/dashboard.py b/backend/app/schemas/dashboard.py new file mode 100644 index 0000000..7ad7bbb --- /dev/null +++ b/backend/app/schemas/dashboard.py @@ -0,0 +1,28 @@ +from datetime import datetime + +from pydantic import BaseModel + + +class StorageStats(BaseModel): + total: int + used: int + free: int + folder_used: int + usage_percent: float + + +class OperationLogResponse(BaseModel): + id: int + message: str + timestamp: datetime + + model_config = {"from_attributes": True} + + +class DashboardResponse(BaseModel): + routers_count: int + export_count: int + binary_count: int + total_backups: int + storage: StorageStats + recent_logs: list[OperationLogResponse] diff --git a/backend/app/schemas/router.py b/backend/app/schemas/router.py new file mode 100644 index 0000000..d63cf46 --- /dev/null +++ b/backend/app/schemas/router.py @@ -0,0 +1,60 @@ +import re +from datetime import datetime + +from pydantic import BaseModel, Field, field_validator + +ALLOWED_NAME_REGEX = re.compile(r"^[A-Za-z0-9_-]+$") + + +class RouterBase(BaseModel): + name: str = Field(min_length=1, max_length=120) + host: str = Field(min_length=1, max_length=255) + port: int = Field(default=22, ge=1, le=65535) + ssh_user: str = Field(default="admin", min_length=1, max_length=120) + ssh_key: str | None = None + ssh_password: str | None = None + + @field_validator("name") + @classmethod + def validate_name(cls, value: str) -> str: + if not ALLOWED_NAME_REGEX.match(value): + raise ValueError("Only letters, digits, dashes and underscores are allowed") + return value + + +class RouterCreate(RouterBase): + pass + + +class RouterUpdate(BaseModel): + name: str | None = None + host: str | None = None + port: int | None = Field(default=None, ge=1, le=65535) + ssh_user: str | None = None + ssh_key: str | None = None + ssh_password: str | None = None + + +class RouterResponse(RouterBase): + id: int + owner_id: int + last_connection_status: bool | None = None + last_connection_tested_at: datetime | None = None + last_connection_error: str | None = None + last_connection_hostname: str | None = None + last_connection_model: str | None = None + last_connection_version: str | None = None + last_connection_uptime: str | None = None + created_at: datetime | None = None + + model_config = {"from_attributes": True} + + +class RouterTestConnection(BaseModel): + success: bool + tested_at: datetime + model: str + uptime: str + hostname: str + version: str | None = None + error: str | None = None diff --git a/backend/app/schemas/settings.py b/backend/app/schemas/settings.py new file mode 100644 index 0000000..36348b5 --- /dev/null +++ b/backend/app/schemas/settings.py @@ -0,0 +1,85 @@ +from datetime import datetime + +from pydantic import BaseModel, EmailStr, Field, field_validator + +from app.core.config import settings as app_settings +from app.core.cron_utils import CronValidationError, validate_cron_expression + + +class SettingsBase(BaseModel): + backup_retention_days: int = 7 + log_retention_days: int = 7 + export_cron: str = '' + binary_cron: str = '' + retention_cron: str = '' + enable_auto_export: bool = False + connection_test_interval_minutes: int = Field(default=0, ge=0, le=1440) + global_ssh_key: str | None = None + pushover_token: str | None = None + pushover_userkey: str | None = None + notify_failures_only: bool = True + smtp_host: str | None = None + smtp_port: int = 587 + smtp_login: str | None = None + smtp_password: str | None = None + smtp_notifications_enabled: bool = False + recipient_email: EmailStr | None = None + + @field_validator('export_cron', 'binary_cron', 'retention_cron', mode='before') + @classmethod + def normalize_cron(cls, value: str | None) -> str: + return (value or '').strip() + + @field_validator('global_ssh_key', mode='before') + @classmethod + def normalize_key(cls, value: str | None) -> str | None: + normalized = (value or '').strip() + return normalized or None + + @field_validator('export_cron', 'binary_cron', 'retention_cron') + @classmethod + def validate_cron(cls, value: str) -> str: + if not value: + return value + try: + validate_cron_expression(value, app_settings.timezone) + except CronValidationError as exc: + raise ValueError(f'Invalid cron expression: {exc}') from exc + return value + + +class SettingsUpdate(SettingsBase): + clear_global_ssh_key: bool = False + + +class SettingsResponse(SettingsBase): + id: int + has_global_ssh_key: bool = False + + model_config = {'from_attributes': True} + + +class RevealSshKeyRequest(BaseModel): + password: str + + +class RevealSshKeyResponse(BaseModel): + global_ssh_key: str | None = None + + +class SchedulerJobStatus(BaseModel): + key: str + label: str + enabled: bool + cron: str | None = None + description: str + description_params: dict[str, str | int] | None = None + valid: bool + next_runs: list[datetime] = [] + error: str | None = None + + +class SchedulerStatusResponse(BaseModel): + timezone: str + running: bool + jobs: list[SchedulerJobStatus] diff --git a/backend/app/schemas/swos_beta.py b/backend/app/schemas/swos_beta.py new file mode 100644 index 0000000..be1079a --- /dev/null +++ b/backend/app/schemas/swos_beta.py @@ -0,0 +1,33 @@ +from pydantic import BaseModel, Field, field_validator + + +class SwosBetaCredentials(BaseModel): + host: str = Field(min_length=1, max_length=255) + port: int = Field(default=80, ge=1, le=65535) + username: str = Field(default='admin', min_length=1, max_length=120) + password: str = Field(default='', max_length=255) + label: str | None = Field(default=None, max_length=120) + + @field_validator('host', 'username', 'password', mode='before') + @classmethod + def normalize_text(cls, value: str | None) -> str: + return (value or '').strip() + + @field_validator('label', mode='before') + @classmethod + def normalize_label(cls, value: str | None) -> str | None: + normalized = (value or '').strip() + return normalized or None + + +class SwosBetaProbeResponse(BaseModel): + success: bool + base_url: str + status_code: int + auth_mode: str + page_title: str | None = None + content_type: str | None = None + server: str | None = None + save_backup_visible: bool = False + backup_endpoint_ok: bool = False + note: str | None = None diff --git a/backend/app/services/backup_service.py b/backend/app/services/backup_service.py new file mode 100644 index 0000000..377f695 --- /dev/null +++ b/backend/app/services/backup_service.py @@ -0,0 +1,321 @@ +import difflib +from datetime import datetime, timedelta, timezone +from pathlib import Path + +from fastapi import HTTPException +from sqlalchemy.orm import Session, joinedload + +from app.models.backup import Backup +from app.models.router import Router +from app.models.user import User +from app.services.file_service import compute_checksum, ensure_data_dir +from app.services.log_service import log_service +from app.services.notification_service import notification_service +from app.services.router_service import router_service +from app.services.settings_service import settings_service + + +class BackupService: + def _router_for_user(self, db: Session, user: User, router_id: int) -> Router: + router = db.query(Router).filter(Router.id == router_id, Router.owner_id == user.id).first() + if not router: + raise HTTPException(status_code=404, detail='Router not found') + return router + + def _serialize_backup(self, backup: Backup): + file_path = Path(backup.file_path) + return { + 'id': backup.id, + 'router_id': backup.router_id, + 'router_name': backup.router.name if backup.router else None, + 'file_path': backup.file_path, + 'file_name': backup.file_name, + 'backup_type': backup.backup_type, + 'checksum': backup.checksum, + 'file_size': file_path.stat().st_size if file_path.exists() else None, + 'created_at': backup.created_at, + } + + def _build_structured_diff(self, left_lines: list[str], right_lines: list[str]): + matcher = difflib.SequenceMatcher(a=left_lines, b=right_lines) + rows = [] + stats = {'added': 0, 'removed': 0, 'modified': 0, 'context': 0} + left_number = 1 + right_number = 1 + + for tag, i1, i2, j1, j2 in matcher.get_opcodes(): + if tag == 'equal': + for left_text, right_text in zip(left_lines[i1:i2], right_lines[j1:j2]): + rows.append( + { + 'type': 'context', + 'left_number': left_number, + 'right_number': right_number, + 'left_text': left_text, + 'right_text': right_text, + } + ) + stats['context'] += 1 + left_number += 1 + right_number += 1 + continue + + if tag == 'delete': + for left_text in left_lines[i1:i2]: + rows.append( + { + 'type': 'removed', + 'left_number': left_number, + 'right_number': None, + 'left_text': left_text, + 'right_text': '', + } + ) + stats['removed'] += 1 + left_number += 1 + continue + + if tag == 'insert': + for right_text in right_lines[j1:j2]: + rows.append( + { + 'type': 'added', + 'left_number': None, + 'right_number': right_number, + 'left_text': '', + 'right_text': right_text, + } + ) + stats['added'] += 1 + right_number += 1 + continue + + block_left = left_lines[i1:i2] + block_right = right_lines[j1:j2] + block_size = max(len(block_left), len(block_right)) + for index in range(block_size): + left_text = block_left[index] if index < len(block_left) else '' + right_text = block_right[index] if index < len(block_right) else '' + row_type = 'modified' + if left_text and not right_text: + row_type = 'removed' + stats['removed'] += 1 + elif right_text and not left_text: + row_type = 'added' + stats['added'] += 1 + else: + stats['modified'] += 1 + + rows.append( + { + 'type': row_type, + 'left_number': left_number if left_text else None, + 'right_number': right_number if right_text else None, + 'left_text': left_text, + 'right_text': right_text, + } + ) + if left_text: + left_number += 1 + if right_text: + right_number += 1 + + return rows, stats + + def get_backup_for_user(self, db: Session, user: User, backup_id: int) -> Backup: + backup = ( + db.query(Backup) + .options(joinedload(Backup.router)) + .join(Router) + .filter(Backup.id == backup_id, Router.owner_id == user.id) + .first() + ) + if not backup: + raise HTTPException(status_code=404, detail='Backup not found') + return backup + + def list_backups( + self, + db: Session, + user: User, + search: str | None = None, + backup_type: str | None = None, + router_id: int | None = None, + sort_by: str = 'created_at', + order: str = 'desc', + ): + query = db.query(Backup).options(joinedload(Backup.router)).join(Router).filter(Router.owner_id == user.id) + if search: + query = query.filter( + Backup.file_name.ilike(f'%{search}%') + | Router.name.ilike(f'%{search}%') + | Router.host.ilike(f'%{search}%') + ) + if backup_type: + query = query.filter(Backup.backup_type == backup_type) + if router_id: + query = query.filter(Backup.router_id == router_id) + + sort_map = { + 'created_at': Backup.created_at, + 'file_name': Backup.file_name, + 'backup_type': Backup.backup_type, + 'router_name': Router.name, + } + sort_column = sort_map.get(sort_by, Backup.created_at) + query = query.order_by(sort_column.asc() if order == 'asc' else sort_column.desc()) + return [self._serialize_backup(backup) for backup in query.all()] + + def list_router_backups(self, db: Session, user: User, router_id: int): + router = self._router_for_user(db, user, router_id) + backups = ( + db.query(Backup) + .options(joinedload(Backup.router)) + .filter(Backup.router_id == router.id) + .order_by(Backup.created_at.desc()) + .all() + ) + return [self._serialize_backup(backup) for backup in backups] + + def export_router(self, db: Session, user: User, router_id: int) -> Backup: + router = self._router_for_user(db, user, router_id) + settings = settings_service.get_or_create(db) + stamp = datetime.now().strftime('%Y%m%d_%H%M%S') + name = f'{router.name}_{router.id}_{stamp}.rsc' + file_path = ensure_data_dir() / name + try: + content = router_service.export(router, settings.global_ssh_key) + file_path.write_text(content, encoding='utf-8') + backup = Backup(router_id=router.id, file_path=str(file_path), file_name=name, backup_type='export') + db.add(backup) + db.commit() + db.refresh(backup) + log_service.add(db, f'Export OK for router {router.name}') + notification_service.notify(settings, f'Export {router.name} OK', True) + return backup + except Exception as exc: + notification_service.notify(settings, f'Export {router.name} FAIL: {exc}', False) + log_service.add(db, f'Export FAILED for router {router.name}: {exc}') + raise HTTPException(status_code=500, detail=str(exc)) from exc + + def binary_backup(self, db: Session, user: User, router_id: int) -> Backup: + router = self._router_for_user(db, user, router_id) + settings = settings_service.get_or_create(db) + stamp = datetime.now().strftime('%Y%m%d_%H%M%S') + base_name = f'{router.name}_{router.id}_{stamp}' + name = f'{base_name}.backup' + file_path = ensure_data_dir() / name + try: + router_service.binary_backup(router, base_name, str(file_path), settings.global_ssh_key) + checksum = compute_checksum(str(file_path)) + backup = Backup(router_id=router.id, file_path=str(file_path), file_name=name, backup_type='binary', checksum=checksum) + db.add(backup) + db.commit() + db.refresh(backup) + log_service.add(db, f'Binary backup OK for router {router.name}') + notification_service.notify(settings, f'Backup {router.name} OK', True) + return backup + except Exception as exc: + notification_service.notify(settings, f'Backup {router.name} FAIL: {exc}', False) + log_service.add(db, f'Binary backup FAILED for router {router.name}: {exc}') + raise HTTPException(status_code=500, detail=str(exc)) from exc + + def upload_backup_to_router(self, db: Session, user: User, router_id: int, backup_id: int): + router = self._router_for_user(db, user, router_id) + backup = self.get_backup_for_user(db, user, backup_id) + if backup.backup_type != 'binary': + raise HTTPException(status_code=400, detail='Only binary backups can be uploaded') + checksum = compute_checksum(backup.file_path) + if backup.checksum and checksum != backup.checksum: + raise HTTPException(status_code=400, detail='Checksum mismatch') + settings = settings_service.get_or_create(db) + router_service.upload_backup(router, backup.file_path, settings.global_ssh_key) + log_service.add(db, f'Upload backup OK for router {router.name}') + + def delete_backup(self, db: Session, user: User, backup_id: int, commit: bool = True): + backup = self.get_backup_for_user(db, user, backup_id) + path = Path(backup.file_path) + if path.exists(): + path.unlink() + db.delete(backup) + if commit: + db.commit() + + def diff_backups(self, db: Session, user: User, left_id: int, right_id: int): + left = self.get_backup_for_user(db, user, left_id) + right = self.get_backup_for_user(db, user, right_id) + if left.backup_type != 'export' or right.backup_type != 'export': + raise HTTPException(status_code=400, detail='Diff is supported only for export backups') + left_lines = Path(left.file_path).read_text(encoding='utf-8', errors='ignore').splitlines() + right_lines = Path(right.file_path).read_text(encoding='utf-8', errors='ignore').splitlines() + diff_lines = list( + difflib.unified_diff(left_lines, right_lines, fromfile=left.file_name, tofile=right.file_name, lineterm='') + ) + diff_html = difflib.HtmlDiff(wrapcolumn=120).make_file( + left_lines, + right_lines, + fromdesc=left.file_name, + todesc=right.file_name, + context=True, + numlines=2, + ) + structured_lines, stats = self._build_structured_diff(left_lines, right_lines) + return { + 'left_backup_id': left_id, + 'right_backup_id': right_id, + 'left_file_name': left.file_name, + 'right_file_name': right.file_name, + 'diff_text': '\n'.join(diff_lines), + 'diff_html': diff_html, + 'stats': stats, + 'lines': structured_lines, + } + + def email_backup(self, db: Session, user: User, backup_id: int): + backup = self.get_backup_for_user(db, user, backup_id) + settings = settings_service.get_or_create(db) + noun = 'Export' if backup.backup_type == 'export' else 'Backup' + subject = f'RouterOS {noun}: {backup.file_name}' + body = f'Sending {backup.file_name} from router {backup.router.name}.' + notification_service.send_email(settings, subject, body, backup.file_path) + log_service.add(db, f'Email sent for backup {backup.file_name}') + + def export_all(self, db: Session, user: User): + routers = db.query(Router).filter(Router.owner_id == user.id).all() + result = [] + for router in routers: + try: + backup = self.export_router(db, user, router.id) + result.append({'router': router.name, 'status': 'ok', 'backup_id': backup.id}) + except Exception as exc: + result.append({'router': router.name, 'status': 'error', 'message': str(exc)}) + return result + + def binary_all(self, db: Session, user: User): + routers = db.query(Router).filter(Router.owner_id == user.id).all() + result = [] + for router in routers: + try: + backup = self.binary_backup(db, user, router.id) + result.append({'router': router.name, 'status': 'ok', 'backup_id': backup.id}) + except Exception as exc: + result.append({'router': router.name, 'status': 'error', 'message': str(exc)}) + return result + + def cleanup_old_backups(self, db: Session): + settings = settings_service.get_or_create(db) + cutoff = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(days=settings.backup_retention_days) + old_backups = db.query(Backup).filter(Backup.created_at < cutoff).all() + deleted_count = 0 + for backup in old_backups: + path = Path(backup.file_path) + if path.exists(): + path.unlink() + db.delete(backup) + deleted_count += 1 + db.commit() + log_service.add(db, f'Retention cleanup removed {deleted_count} backups older than {settings.backup_retention_days} days') + return deleted_count + + +backup_service = BackupService() diff --git a/backend/app/services/file_service.py b/backend/app/services/file_service.py new file mode 100644 index 0000000..d0ff758 --- /dev/null +++ b/backend/app/services/file_service.py @@ -0,0 +1,38 @@ +import hashlib +import os +import shutil +from pathlib import Path + +from app.core.config import settings +from app.schemas.dashboard import StorageStats + + +def compute_checksum(file_path: str) -> str: + sha256 = hashlib.sha256() + with open(file_path, "rb") as handle: + for chunk in iter(lambda: handle.read(4096), b""): + sha256.update(chunk) + return sha256.hexdigest() + + +def ensure_data_dir() -> Path: + return settings.data_path + + +def get_folder_size() -> int: + total = 0 + for dirpath, _, filenames in os.walk(ensure_data_dir()): + for filename in filenames: + try: + total += os.path.getsize(Path(dirpath) / filename) + except OSError: + pass + return total + + +def get_storage_stats() -> StorageStats: + ensure_data_dir() + disk = shutil.disk_usage(ensure_data_dir()) + folder_used = get_folder_size() + usage_percent = (folder_used / disk.total) * 100 if disk.total else 0 + return StorageStats(total=disk.total, used=disk.used, free=disk.free, folder_used=folder_used, usage_percent=usage_percent) diff --git a/backend/app/services/log_service.py b/backend/app/services/log_service.py new file mode 100644 index 0000000..83dcaa3 --- /dev/null +++ b/backend/app/services/log_service.py @@ -0,0 +1,24 @@ +from datetime import datetime, timedelta, timezone + +from sqlalchemy.orm import Session + +from app.models.log import OperationLog + + +class LogService: + def add(self, db: Session, message: str, commit: bool = True) -> None: + db.add(OperationLog(message=message)) + if commit: + db.commit() + + def delete_older_than(self, db: Session, days: int) -> int: + cutoff = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(days=days) + logs = db.query(OperationLog).filter(OperationLog.timestamp < cutoff).all() + count = len(logs) + for log in logs: + db.delete(log) + db.commit() + return count + + +log_service = LogService() diff --git a/backend/app/services/notification_service.py b/backend/app/services/notification_service.py new file mode 100644 index 0000000..231e72f --- /dev/null +++ b/backend/app/services/notification_service.py @@ -0,0 +1,78 @@ +import smtplib +from email import encoders +from email.mime.base import MIMEBase +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from pathlib import Path + +import requests + +from app.core.config import settings as app_settings +from app.models.settings import GlobalSettings + + +class NotificationService: + def send_pushover(self, token: str, user_key: str, message: str, title: str = "RouterOS Backup") -> bool: + response = requests.post( + "https://api.pushover.net/1/messages.json", + data={"token": token, "user": user_key, "message": message, "title": title}, + timeout=15, + ) + return response.ok + + def send_email(self, settings: GlobalSettings, subject: str, body: str, attachment_path: str | None = None): + if not (settings.smtp_host and settings.smtp_login and settings.smtp_password): + raise ValueError("SMTP is not configured") + recipient = (settings.recipient_email or settings.smtp_login or "").strip() + if not recipient: + raise ValueError("Recipient email is empty") + + msg = MIMEMultipart() + msg["From"] = settings.smtp_login + msg["To"] = recipient + msg["Subject"] = subject + msg.attach(MIMEText(body, "plain", "utf-8")) + + if attachment_path: + attachment = Path(attachment_path) + with attachment.open("rb") as handle: + part = MIMEBase("application", "octet-stream") + part.set_payload(handle.read()) + encoders.encode_base64(part) + part.add_header("Content-Disposition", f'attachment; filename="{attachment.name}"') + msg.attach(part) + + with smtplib.SMTP(settings.smtp_host, settings.smtp_port, timeout=app_settings.smtp_timeout_seconds) as server: + if app_settings.smtp_starttls: + server.starttls() + server.login(settings.smtp_login, settings.smtp_password) + server.sendmail(settings.smtp_login, [recipient], msg.as_string()) + + def notify(self, settings: GlobalSettings, message: str, success: bool): + if settings.notify_failures_only and success: + return + if settings.smtp_notifications_enabled: + try: + self.send_email(settings, "RouterOS Backup notification", message) + except Exception: + pass + if settings.pushover_token and settings.pushover_userkey: + try: + self.send_pushover(settings.pushover_token, settings.pushover_userkey, message) + except Exception: + pass + + def send_test_email(self, settings: GlobalSettings): + self.send_email(settings, "RouterOS Backup test", "This is a test email from RouterOS Backup Manager Next") + + def send_test_pushover(self, settings: GlobalSettings): + if not (settings.pushover_token and settings.pushover_userkey): + raise ValueError("Pushover is not configured") + self.send_pushover( + settings.pushover_token, + settings.pushover_userkey, + "Test pushover from RouterOS Backup Manager Next", + ) + + +notification_service = NotificationService() diff --git a/backend/app/services/router_service.py b/backend/app/services/router_service.py new file mode 100644 index 0000000..36eb450 --- /dev/null +++ b/backend/app/services/router_service.py @@ -0,0 +1,140 @@ +from datetime import datetime +import io +from pathlib import Path + +import paramiko +from sqlalchemy.orm import Session + +from app.models.router import Router + + +class RouterService: + def _load_pkey(self, ssh_key_str: str): + key_str = (ssh_key_str or "").strip() + key_buffer = io.StringIO(key_str) + loaders = [ + paramiko.RSAKey.from_private_key, + paramiko.Ed25519Key.from_private_key, + paramiko.ECDSAKey.from_private_key, + ] + last_error = None + for loader in loaders: + key_buffer.seek(0) + try: + return loader(key_buffer) + except Exception as exc: + last_error = exc + raise ValueError("Failed to load SSH private key") from last_error + + def _connect(self, router: Router, global_ssh_key: str | None = None): + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + key_source = router.ssh_key.strip() if router.ssh_key and router.ssh_key.strip() else (global_ssh_key or "") + if key_source: + pkey = self._load_pkey(key_source) + client.connect(router.host, port=router.port, username=router.ssh_user, pkey=pkey, timeout=10) + else: + client.connect( + router.host, + port=router.port, + username=router.ssh_user, + password=router.ssh_password, + timeout=10, + allow_agent=False, + look_for_keys=False, + banner_timeout=10, + ) + return client + + def export(self, router: Router, global_ssh_key: str | None = None) -> str: + client = self._connect(router, global_ssh_key) + _, stdout, _ = client.exec_command("/export") + output = stdout.read().decode("utf-8", errors="ignore") + client.close() + return output + + def binary_backup(self, router: Router, backup_name: str, local_path: str, global_ssh_key: str | None = None) -> str: + client = self._connect(router, global_ssh_key) + _, stdout, _ = client.exec_command(f"/system backup save name={backup_name}") + stdout.channel.recv_exit_status() + sftp = client.open_sftp() + remote_file = f"{backup_name}.backup" + sftp.get(remote_file, local_path) + try: + sftp.remove(remote_file) + except Exception: + pass + sftp.close() + client.close() + return local_path + + def upload_backup(self, router: Router, local_backup_path: str, global_ssh_key: str | None = None): + client = self._connect(router, global_ssh_key) + sftp = client.open_sftp() + target_name = Path(local_backup_path).name + sftp.put(local_backup_path, target_name) + sftp.close() + client.close() + + def probe_connection(self, router: Router, global_ssh_key: str | None = None): + tested_at = datetime.utcnow() + try: + client = self._connect(router, global_ssh_key) + _, stdout, _ = client.exec_command("/system resource print without-paging") + resource_output = stdout.read().decode("utf-8", errors="ignore") + _, stdout, _ = client.exec_command("/system identity print") + identity_output = stdout.read().decode("utf-8", errors="ignore") + client.close() + model = "Unknown" + uptime = "Unknown" + hostname = "Unknown" + version = "Unknown" + for line in resource_output.splitlines(): + if "board-name" in line: + model = line.split(":", 1)[1].strip() + if "uptime" in line: + uptime = line.split(":", 1)[1].strip() + if "version" in line: + version = line.split(":", 1)[1].strip() + for line in identity_output.splitlines(): + if "name" in line: + hostname = line.split(":", 1)[1].strip() + return { + "success": True, + "tested_at": tested_at, + "model": model, + "uptime": uptime, + "hostname": hostname, + "version": version, + "error": None, + } + except Exception as exc: + return { + "success": False, + "tested_at": tested_at, + "model": "Unknown", + "uptime": "Unknown", + "hostname": router.name, + "version": None, + "error": str(exc), + } + + def _store_connection_result(self, db: Session, router: Router, result: dict): + router.last_connection_status = result["success"] + router.last_connection_tested_at = result["tested_at"] + router.last_connection_error = result.get("error") + router.last_connection_hostname = result.get("hostname") + router.last_connection_model = result.get("model") + router.last_connection_version = result.get("version") + router.last_connection_uptime = result.get("uptime") + db.add(router) + db.commit() + db.refresh(router) + return result + + def test_connection(self, db: Session, router: Router, global_ssh_key: str | None = None): + result = self.probe_connection(router, global_ssh_key) + return self._store_connection_result(db, router, result) + + +router_service = RouterService() diff --git a/backend/app/services/scheduler.py b/backend/app/services/scheduler.py new file mode 100644 index 0000000..7394790 --- /dev/null +++ b/backend/app/services/scheduler.py @@ -0,0 +1,249 @@ +from __future__ import annotations + +from datetime import datetime, timedelta + +from apscheduler.schedulers.background import BackgroundScheduler + +from app.core.config import settings as app_settings +from app.core.cron_utils import CronValidationError, describe_cron_expression, parse_cron_expression, preview_next_runs +from app.db.session import SessionLocal +from app.models.router import Router +from app.services.backup_service import backup_service +from app.services.log_service import log_service +from app.services.router_service import router_service +from app.services.settings_service import settings_service + + +class SchedulerService: + def __init__(self): + self.scheduler = BackgroundScheduler(timezone=app_settings.timezone) + self.started = False + + def start(self): + if self.started: + return + self.reschedule() + self.scheduler.start() + self.started = True + + def shutdown(self): + if self.started: + self.scheduler.shutdown(wait=False) + self.started = False + + def _parse_cron(self, expr: str): + return parse_cron_expression(expr, app_settings.timezone) + + def validate_cron(self, expr: str): + return self._parse_cron(expr) + + def _interval_next_runs(self, minutes: int, count: int = 3): + now = datetime.now() + return [now + timedelta(minutes=minutes * index) for index in range(1, count + 1)] + + def scheduler_status(self): + with SessionLocal() as db: + settings = settings_service.get_or_create(db) + return { + 'timezone': app_settings.timezone, + 'running': self.started, + 'jobs': [ + self._describe_job( + key='auto_export', + label='settings.schedulerAutoExportLabel', + enabled=settings.enable_auto_export, + cron=settings.export_cron, + ), + self._describe_job( + key='auto_binary', + label='settings.schedulerBinaryLabel', + enabled=bool(settings.binary_cron), + cron=settings.binary_cron, + ), + self._describe_job( + key='retention', + label='settings.schedulerRetentionLabel', + enabled=bool(settings.retention_cron), + cron=settings.retention_cron, + ), + self._describe_interval_job( + key='connection_probe', + label='settings.schedulerConnectionLabel', + minutes=settings.connection_test_interval_minutes, + ), + { + 'key': 'log_cleanup', + 'label': 'settings.schedulerLogsLabel', + 'enabled': True, + 'cron': None, + 'description': 'settings.schedulerLogsDescription', + 'description_params': None, + 'valid': True, + 'next_runs': [], + 'error': None, + }, + ], + } + + def _describe_job(self, key: str, label: str, enabled: bool, cron: str | None): + cron = (cron or '').strip() + if not enabled or not cron: + return { + 'key': key, + 'label': label, + 'enabled': False, + 'cron': cron or None, + 'description': 'settings.scheduleDisabledHint', + 'description_params': None, + 'valid': True, + 'next_runs': [], + 'error': None, + } + try: + next_runs = preview_next_runs(cron, app_settings.timezone, count=3) + return { + 'key': key, + 'label': label, + 'enabled': True, + 'cron': cron, + 'description': 'settings.schedulerCronDescription', + 'description_params': {'description': describe_cron_expression(cron)}, + 'valid': True, + 'next_runs': next_runs, + 'error': None, + } + except CronValidationError as exc: + return { + 'key': key, + 'label': label, + 'enabled': True, + 'cron': cron, + 'description': 'settings.schedulerInvalidCron', + 'description_params': None, + 'valid': False, + 'next_runs': [], + 'error': str(exc), + } + + def _describe_interval_job(self, key: str, label: str, minutes: int): + minutes = int(minutes or 0) + if minutes <= 0: + return { + 'key': key, + 'label': label, + 'enabled': False, + 'cron': None, + 'description': 'settings.connectionTestsDisabledHint', + 'description_params': None, + 'valid': True, + 'next_runs': [], + 'error': None, + } + return { + 'key': key, + 'label': label, + 'enabled': True, + 'cron': None, + 'description': 'settings.connectionTestsEverySummary', + 'description_params': {'minutes': minutes}, + 'valid': True, + 'next_runs': self._interval_next_runs(minutes), + 'error': None, + } + + def reschedule(self): + self.scheduler.remove_all_jobs() + with SessionLocal() as db: + settings = settings_service.get_or_create(db) + job_definitions = [ + ('auto_export', settings.enable_auto_export, settings.export_cron, self._run_auto_export, 'auto export'), + ('auto_binary', bool(settings.binary_cron), settings.binary_cron, self._run_binary_backup, 'binary backup'), + ('retention', bool(settings.retention_cron), settings.retention_cron, self._run_retention, 'retention cleanup'), + ] + + pending_logs: list[str] = [] + for job_id, enabled, cron, callback, label in job_definitions: + cron = (cron or '').strip() + if not enabled or not cron: + continue + try: + trigger = self._parse_cron(cron) + self.scheduler.add_job( + callback, + trigger=trigger, + id=job_id, + replace_existing=True, + coalesce=True, + max_instances=1, + misfire_grace_time=300, + ) + except Exception as exc: + pending_logs.append(f'Scheduler skipped invalid {label} cron ({cron}): {exc}') + + if int(settings.connection_test_interval_minutes or 0) > 0: + self.scheduler.add_job( + self._run_connection_probes, + trigger='interval', + minutes=int(settings.connection_test_interval_minutes), + id='connection_probe', + replace_existing=True, + coalesce=True, + max_instances=1, + misfire_grace_time=300, + ) + + self.scheduler.add_job( + self._run_log_cleanup, + trigger='interval', + days=1, + id='log_cleanup', + replace_existing=True, + coalesce=True, + max_instances=1, + misfire_grace_time=300, + ) + + for message in pending_logs: + log_service.add(db, message, commit=False) + if pending_logs: + db.commit() + + def _run_auto_export(self): + with SessionLocal() as db: + routers = db.query(Router).all() + for router in routers: + try: + backup_service.export_router(db, type('U', (), {'id': router.owner_id})(), router.id) + except Exception as exc: + log_service.add(db, f'Scheduled export failed for {router.name}: {exc}') + + def _run_binary_backup(self): + with SessionLocal() as db: + routers = db.query(Router).all() + for router in routers: + try: + backup_service.binary_backup(db, type('U', (), {'id': router.owner_id})(), router.id) + except Exception as exc: + log_service.add(db, f'Scheduled binary backup failed for {router.name}: {exc}') + + def _run_retention(self): + with SessionLocal() as db: + backup_service.cleanup_old_backups(db) + + def _run_connection_probes(self): + with SessionLocal() as db: + settings = settings_service.get_or_create(db) + routers = db.query(Router).all() + for router in routers: + result = router_service.test_connection(db, router, settings.global_ssh_key) + if not result['success']: + log_service.add(db, f'Scheduled connection test failed for {router.name}: {result.get("error") or "Unknown error"}') + + def _run_log_cleanup(self): + with SessionLocal() as db: + settings = settings_service.get_or_create(db) + deleted = log_service.delete_older_than(db, settings.log_retention_days) + log_service.add(db, f'Log retention cleanup removed {deleted} entries older than {settings.log_retention_days} days') + + +scheduler_service = SchedulerService() diff --git a/backend/app/services/settings_service.py b/backend/app/services/settings_service.py new file mode 100644 index 0000000..7ef52e3 --- /dev/null +++ b/backend/app/services/settings_service.py @@ -0,0 +1,32 @@ +from sqlalchemy.orm import Session + +from app.models.settings import GlobalSettings +from app.schemas.settings import SettingsUpdate + + +class SettingsService: + def get_or_create(self, db: Session) -> GlobalSettings: + settings = db.query(GlobalSettings).first() + if not settings: + settings = GlobalSettings() + db.add(settings) + db.commit() + db.refresh(settings) + return settings + + def update(self, db: Session, payload: SettingsUpdate) -> GlobalSettings: + settings = self.get_or_create(db) + data = payload.model_dump(exclude={'clear_global_ssh_key'}) + for key, value in data.items(): + if key == 'global_ssh_key' and value is None and not payload.clear_global_ssh_key: + continue + setattr(settings, key, value) + if payload.clear_global_ssh_key: + settings.global_ssh_key = None + db.add(settings) + db.commit() + db.refresh(settings) + return settings + + +settings_service = SettingsService() diff --git a/backend/app/services/swos_beta_service.py b/backend/app/services/swos_beta_service.py new file mode 100644 index 0000000..18cf704 --- /dev/null +++ b/backend/app/services/swos_beta_service.py @@ -0,0 +1,124 @@ +import re +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from urllib.parse import urlparse + +import requests +from requests.auth import HTTPBasicAuth, HTTPDigestAuth + +from app.schemas.swos_beta import SwosBetaCredentials, SwosBetaProbeResponse + + +@dataclass +class DownloadedSwosBackup: + filename: str + content: bytes + content_type: str + auth_mode: str + base_url: str + + +class SwosBetaService: + timeout_seconds = 12 + + def probe(self, payload: SwosBetaCredentials) -> SwosBetaProbeResponse: + base_url = self._build_base_url(payload.host, payload.port) + response, auth_mode = self._request_with_fallback('GET', base_url, payload) + html = response.text if 'text' in (response.headers.get('content-type') or '').lower() else '' + title = self._extract_title(html) + + backup_response, _ = self._request_with_fallback('GET', f'{base_url}/backup.swb', payload, allow_text_fallback=False) + backup_ok = backup_response.status_code == 200 and len(backup_response.content) > 0 + + return SwosBetaProbeResponse( + success=response.ok, + base_url=base_url, + status_code=response.status_code, + auth_mode=auth_mode, + page_title=title, + content_type=response.headers.get('content-type'), + server=response.headers.get('server'), + save_backup_visible='save backup' in html.lower(), + backup_endpoint_ok=backup_ok, + note='Moduł działa osobno i nie zapisuje kopii do głównego repozytorium.' + ) + + def download_backup(self, payload: SwosBetaCredentials) -> DownloadedSwosBackup: + base_url = self._build_base_url(payload.host, payload.port) + response, auth_mode = self._request_with_fallback('GET', f'{base_url}/backup.swb', payload, allow_text_fallback=False) + if response.status_code != 200: + raise ValueError(f'Urządzenie zwróciło kod HTTP {response.status_code} dla /backup.swb.') + if not response.content: + raise ValueError('Urządzenie zwróciło pusty plik backupu.') + + filename = self._build_filename(payload) + content_type = response.headers.get('content-type') or 'application/octet-stream' + return DownloadedSwosBackup( + filename=filename, + content=response.content, + content_type=content_type, + auth_mode=auth_mode, + base_url=base_url, + ) + + def _request_with_fallback(self, method: str, url: str, payload: SwosBetaCredentials, allow_text_fallback: bool = True): + attempts = [] + auth_variants = [ + ('digest', HTTPDigestAuth(payload.username, payload.password)), + ('basic', HTTPBasicAuth(payload.username, payload.password)), + ] + if allow_text_fallback: + auth_variants.append(('none', None)) + + last_response = None + for label, auth in auth_variants: + try: + response = requests.request( + method, + url, + auth=auth, + timeout=self.timeout_seconds, + allow_redirects=True, + ) + last_response = response + if response.status_code < 400: + return response, label + attempts.append(f'{label}:{response.status_code}') + except requests.RequestException as exc: + attempts.append(f'{label}:{exc.__class__.__name__}') + + if last_response is not None: + raise ValueError(f'Nie udało się połączyć ze SwOS ({", ".join(attempts)}).') + raise ValueError('Nie udało się połączyć ze SwOS.') + + def _build_base_url(self, host: str, port: int) -> str: + raw = host.strip() + parsed = urlparse(raw if '://' in raw else f'http://{raw}') + scheme = parsed.scheme or 'http' + if scheme not in {'http', 'https'}: + raise ValueError('Dozwolone są tylko adresy HTTP lub HTTPS.') + if not parsed.hostname: + raise ValueError('Nieprawidłowy adres hosta.') + resolved_port = parsed.port or port + base = f'{scheme}://{parsed.hostname}' + if resolved_port not in {80, 443} or (scheme == 'http' and resolved_port != 80) or (scheme == 'https' and resolved_port != 443): + base = f'{base}:{resolved_port}' + return base.rstrip('/') + + def _extract_title(self, html: str) -> str | None: + if not html: + return None + match = re.search(r'(.*?)', html, flags=re.IGNORECASE | re.DOTALL) + if not match: + return None + return re.sub(r'\s+', ' ', match.group(1)).strip() or None + + def _build_filename(self, payload: SwosBetaCredentials) -> str: + label = payload.label or payload.host + safe = re.sub(r'[^A-Za-z0-9._-]+', '-', label).strip('-') or 'switchos' + timestamp = datetime.now().strftime('%Y%m%d-%H%M%S') + return f'{safe}-swos-{timestamp}.swb' + + +swos_beta_service = SwosBetaService() diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..89bc6e1 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,14 @@ +fastapi[standard]==0.135.2 +sqlalchemy==2.0.49 +pydantic==2.12.5 +pydantic-settings==2.13.1 +python-jose[cryptography]==3.5.0 +passlib==1.7.4 +python-multipart==0.0.20 +paramiko==3.5.1 +apscheduler==3.11.0 +requests==2.32.3 +alembic==1.15.2 +email-validator==2.2.0 +pytest==8.3.5 +httpx==0.28.1 diff --git a/backend/scripts/migrate_legacy_sqlite.py b/backend/scripts/migrate_legacy_sqlite.py new file mode 100755 index 0000000..1014345 --- /dev/null +++ b/backend/scripts/migrate_legacy_sqlite.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +"""Import data from the original Flask SQLite database into the new schema. + +Usage: + python backend/scripts/migrate_legacy_sqlite.py /path/to/backup_routeros.db +""" + +from __future__ import annotations + +import sqlite3 +import sys +from datetime import datetime +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from app.core.security import get_password_hash # noqa: E402 +from app.db.session import SessionLocal, init_db # noqa: E402 +from app.models.backup import Backup # noqa: E402 +from app.models.log import OperationLog # noqa: E402 +from app.models.router import Router # noqa: E402 +from app.models.settings import GlobalSettings # noqa: E402 +from app.models.user import User # noqa: E402 + + +def parse_dt(value): + if not value: + return None + for fmt in ("%Y-%m-%d %H:%M:%S.%f", "%Y-%m-%d %H:%M:%S"): + try: + return datetime.strptime(value, fmt) + except ValueError: + pass + return None + + +def main() -> int: + if len(sys.argv) != 2: + print("Usage: python backend/scripts/migrate_legacy_sqlite.py /path/to/legacy.db") + return 1 + + source_path = Path(sys.argv[1]).resolve() + if not source_path.exists(): + print(f"Legacy DB not found: {source_path}") + return 1 + + init_db() + source = sqlite3.connect(str(source_path)) + source.row_factory = sqlite3.Row + dest = SessionLocal() + + try: + user_map: dict[int, int] = {} + for row in source.execute("SELECT id, username, password_hash FROM users ORDER BY id"): + existing = dest.query(User).filter(User.username == row["username"]).first() + if existing: + user_map[row["id"]] = existing.id + continue + user = User(username=row["username"], password_hash=row["password_hash"] or get_password_hash("admin")) + dest.add(user) + dest.flush() + user_map[row["id"]] = user.id + + router_map: dict[int, int] = {} + for row in source.execute( + "SELECT id, owner_id, name, host, port, ssh_user, ssh_key, ssh_password, created_at FROM routers ORDER BY id" + ): + router = Router( + owner_id=user_map.get(row["owner_id"], next(iter(user_map.values()), 1)), + name=row["name"], + host=row["host"], + port=row["port"] or 22, + ssh_user=row["ssh_user"] or "admin", + ssh_key=row["ssh_key"], + ssh_password=row["ssh_password"], + created_at=parse_dt(row["created_at"]), + ) + dest.add(router) + dest.flush() + router_map[row["id"]] = router.id + + for row in source.execute( + "SELECT router_id, file_path, backup_type, created_at, checksum FROM backups ORDER BY id" + ): + file_name = Path(row["file_path"] or "backup").name + backup = Backup( + router_id=router_map[row["router_id"]], + file_path=row["file_path"], + file_name=file_name, + backup_type=row["backup_type"] or "export", + created_at=parse_dt(row["created_at"]), + checksum=row["checksum"], + ) + dest.add(backup) + + for row in source.execute("SELECT message, timestamp FROM operation_logs ORDER BY id"): + dest.add(OperationLog(message=row["message"], timestamp=parse_dt(row["timestamp"]))) + + legacy_settings = source.execute("SELECT * FROM global_settings ORDER BY id LIMIT 1").fetchone() + if legacy_settings: + settings = dest.query(GlobalSettings).first() or GlobalSettings() + for key in legacy_settings.keys(): + if hasattr(settings, key): + setattr(settings, key, legacy_settings[key]) + dest.add(settings) + + dest.commit() + print("Migration completed") + return 0 + finally: + source.close() + dest.close() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py new file mode 100644 index 0000000..6840603 --- /dev/null +++ b/backend/tests/test_auth.py @@ -0,0 +1,35 @@ +from fastapi.testclient import TestClient + +from app.main import app + + +def test_login_accepts_form_and_json(monkeypatch, tmp_path): + monkeypatch.setenv("DATABASE_URL", f"sqlite:///{tmp_path / 'auth.db'}") + monkeypatch.setenv("DATA_DIR", str(tmp_path / 'data')) + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DEFAULT_ADMIN_USERNAME", "admin") + monkeypatch.setenv("DEFAULT_ADMIN_PASSWORD", "admin") + + with TestClient(app) as client: + form_response = client.post("/api/auth/login", data={"username": "admin", "password": "admin"}) + assert form_response.status_code == 200 + assert "access_token" in form_response.json() + + json_response = client.post("/api/auth/login", json={"username": "admin", "password": "admin"}) + assert json_response.status_code == 200 + assert "access_token" in json_response.json() + + +def test_auth_me(monkeypatch, tmp_path): + monkeypatch.setenv("DATABASE_URL", f"sqlite:///{tmp_path / 'me.db'}") + monkeypatch.setenv("DATA_DIR", str(tmp_path / 'data')) + monkeypatch.setenv("SECRET_KEY", "test-secret") + monkeypatch.setenv("DEFAULT_ADMIN_USERNAME", "admin") + monkeypatch.setenv("DEFAULT_ADMIN_PASSWORD", "admin") + + with TestClient(app) as client: + login_response = client.post("/api/auth/login", data={"username": "admin", "password": "admin"}) + token = login_response.json()["access_token"] + me_response = client.get("/api/auth/me", headers={"Authorization": f"Bearer {token}"}) + assert me_response.status_code == 200 + assert me_response.json()["username"] == "admin" diff --git a/backend/tests/test_health.py b/backend/tests/test_health.py new file mode 100644 index 0000000..7d15b5a --- /dev/null +++ b/backend/tests/test_health.py @@ -0,0 +1,10 @@ +from fastapi.testclient import TestClient + +from app.main import app + + +def test_health_endpoint(): + client = TestClient(app) + response = client.get("/api/health") + assert response.status_code == 200 + assert response.json()["status"] in {"ok", "error"} diff --git a/backend/tests/test_scheduler.py b/backend/tests/test_scheduler.py new file mode 100644 index 0000000..1705c6d --- /dev/null +++ b/backend/tests/test_scheduler.py @@ -0,0 +1,24 @@ +from app.core.cron_utils import CronValidationError, describe_cron_expression, preview_next_runs, validate_cron_expression + + +def test_validate_cron_expression_accepts_daily_schedule(): + validate_cron_expression('15 2 * * *', 'Europe/Warsaw') + + +def test_validate_cron_expression_rejects_invalid_schedule(): + try: + validate_cron_expression('bad cron', 'Europe/Warsaw') + except CronValidationError: + assert True + return + assert False, 'invalid cron should raise' + + +def test_preview_next_runs_returns_future_datetimes(): + runs = preview_next_runs('0 3 * * 1', 'Europe/Warsaw', count=2) + assert len(runs) == 2 + assert runs[0] < runs[1] + + +def test_describe_cron_expression_humanizes_common_patterns(): + assert describe_cron_expression('0 2 * * *') == 'Every day at 02:00' diff --git a/backend/tests/test_swos_beta.py b/backend/tests/test_swos_beta.py new file mode 100644 index 0000000..4f858e8 --- /dev/null +++ b/backend/tests/test_swos_beta.py @@ -0,0 +1,74 @@ +from fastapi.testclient import TestClient + +from app.main import app +from app.schemas.swos_beta import SwosBetaProbeResponse + + +def _login(client: TestClient) -> str: + response = client.post('/api/auth/login', data={'username': 'admin', 'password': 'admin'}) + return response.json()['access_token'] + + +def test_swos_probe_endpoint(monkeypatch, tmp_path): + monkeypatch.setenv('DATABASE_URL', f'sqlite:///{tmp_path / "swos_probe.db"}') + monkeypatch.setenv('DATA_DIR', str(tmp_path / 'data')) + monkeypatch.setenv('SECRET_KEY', 'test-secret') + monkeypatch.setenv('DEFAULT_ADMIN_USERNAME', 'admin') + monkeypatch.setenv('DEFAULT_ADMIN_PASSWORD', 'admin') + + from app.api.routes import swos_beta + + monkeypatch.setattr( + swos_beta.swos_beta_service, + 'probe', + lambda payload: SwosBetaProbeResponse( + success=True, + base_url='http://192.168.88.1', + status_code=200, + auth_mode='digest', + page_title='SwOS', + content_type='text/html', + server='MikroTik', + save_backup_visible=True, + backup_endpoint_ok=True, + note='beta', + ), + ) + + with TestClient(app) as client: + token = _login(client) + response = client.post( + '/api/swos-beta/probe', + json={'host': '192.168.88.1', 'port': 80, 'username': 'admin', 'password': ''}, + headers={'Authorization': f'Bearer {token}'}, + ) + assert response.status_code == 200 + assert response.json()['backup_endpoint_ok'] is True + + +def test_swos_download_endpoint(monkeypatch, tmp_path): + monkeypatch.setenv('DATABASE_URL', f'sqlite:///{tmp_path / "swos_download.db"}') + monkeypatch.setenv('DATA_DIR', str(tmp_path / 'data')) + monkeypatch.setenv('SECRET_KEY', 'test-secret') + monkeypatch.setenv('DEFAULT_ADMIN_USERNAME', 'admin') + monkeypatch.setenv('DEFAULT_ADMIN_PASSWORD', 'admin') + + from app.api.routes import swos_beta + + class FakeBackup: + filename = 'switch.swb' + content = b'binary-data' + content_type = 'application/octet-stream' + + monkeypatch.setattr(swos_beta.swos_beta_service, 'download_backup', lambda payload: FakeBackup()) + + with TestClient(app) as client: + token = _login(client) + response = client.post( + '/api/swos-beta/download', + json={'host': '192.168.88.1', 'port': 80, 'username': 'admin', 'password': ''}, + headers={'Authorization': f'Bearer {token}'}, + ) + assert response.status_code == 200 + assert response.content == b'binary-data' + assert 'attachment; filename="switch.swb"' == response.headers['content-disposition'] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c600625 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +services: + backend: + build: + context: . + dockerfile: backend/Dockerfile + container_name: routeros-backup-backend + env_file: + - .env + volumes: + - ./docker-data:/app/storage + healthcheck: + test: ['CMD', 'curl', '-fsS', 'http://localhost:8000/api/health'] + interval: 30s + timeout: 5s + retries: 5 + start_period: 20s + restart: unless-stopped + + reverse-proxy: + build: + context: . + dockerfile: frontend/Dockerfile + container_name: routeros-backup-reverse-proxy + depends_on: + backend: + condition: service_healthy + ports: + - '${APP_PORT:-8080}:80' + restart: unless-stopped \ No newline at end of file diff --git a/env.example b/env.example new file mode 100644 index 0000000..6e10c2e --- /dev/null +++ b/env.example @@ -0,0 +1,9 @@ +SECRET_KEY=change-me-before-production +ACCESS_TOKEN_EXPIRE_MINUTES=7899999 +ALLOW_REGISTRATION=true +DEFAULT_ADMIN_USERNAME=admin +DEFAULT_ADMIN_PASSWORD=admin +APP_PORT=5580 +API_PREFIX=/api +DATA_DIR=/app/storage +DATABASE_URL=sqlite:////app/storage/routeros_backup_next.db \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..248a647 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,12 @@ +FROM node:22-alpine AS build +WORKDIR /app +COPY frontend/package*.json /app/ +RUN npm ci || npm install +COPY frontend /app +RUN npm run build + +FROM nginx:1.29-alpine +COPY frontend/nginx/default.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist/routeros-backup-manager-next-ui/browser /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/angular.json b/frontend/angular.json new file mode 100644 index 0000000..16e94f4 --- /dev/null +++ b/frontend/angular.json @@ -0,0 +1,37 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "projects": { + "routeros-backup-manager-next-ui": { + "projectType": "application", + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/routeros-backup-manager-next-ui", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "assets": ["src/favicon.ico", "src/assets"], + "styles": [ + "node_modules/primeicons/primeicons.css", + "node_modules/primeng/resources/themes/lara-light-blue/theme.css", + "node_modules/primeng/resources/primeng.min.css", + "src/styles.css" + ] + } + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "buildTarget": "routeros-backup-manager-next-ui:build" + } + } + } + } + } +} \ No newline at end of file diff --git a/frontend/nginx/default.conf b/frontend/nginx/default.conf new file mode 100644 index 0000000..dac8bf6 --- /dev/null +++ b/frontend/nginx/default.conf @@ -0,0 +1,78 @@ +upstream backend_upstream { + server backend:8000; + keepalive 16; +} + +server { + listen 80; + server_name _; + + server_tokens off; + etag off; + + root /usr/share/nginx/html; + index index.html; + + #gzip on; + #gzip_comp_level 5; + #gzip_min_length 1024; + #gzip_types text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml; + + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; + + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Connection ""; + proxy_buffering off; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + + location ^~ /assets/ { + expires 7d; + add_header Cache-Control "public, max-age=604800, immutable" always; + try_files $uri =404; + } + + location = /favicon.ico { + expires 7d; + add_header Cache-Control "public, max-age=604800" always; + try_files $uri =404; + } + + location = /index.html { + expires -1; + add_header Cache-Control "no-store, no-cache, must-revalidate" always; + try_files $uri =404; + } + + location ^~ /api/ { + add_header Cache-Control "no-store, no-cache, must-revalidate" always; + add_header Pragma "no-cache" always; + proxy_pass http://backend_upstream/api/; + } + + location = /docs { + add_header Cache-Control "no-store, no-cache, must-revalidate" always; + proxy_pass http://backend_upstream/docs; + } + + location = /openapi.json { + add_header Cache-Control "no-store, no-cache, must-revalidate" always; + proxy_pass http://backend_upstream/openapi.json; + } + + location = /redoc { + add_header Cache-Control "no-store, no-cache, must-revalidate" always; + proxy_pass http://backend_upstream/redoc; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..6949c11 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,12730 @@ +{ + "name": "routeros-backup-manager-next-ui", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "routeros-backup-manager-next-ui", + "version": "1.0.0", + "dependencies": { + "@angular/animations": "^17.3.0", + "@angular/common": "^17.3.0", + "@angular/compiler": "^17.3.0", + "@angular/core": "^17.3.0", + "@angular/forms": "^17.3.0", + "@angular/platform-browser": "^17.3.0", + "@angular/platform-browser-dynamic": "^17.3.0", + "@angular/router": "^17.3.0", + "@ngx-translate/core": "^15.0.0", + "@ngx-translate/http-loader": "^8.0.0", + "primeicons": "^7.0.0", + "primeng": "^17.18.0", + "rxjs": "^7.8.1", + "tslib": "^2.6.2", + "zone.js": "^0.14.4" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^17.3.0", + "@angular/cli": "^17.3.0", + "@angular/compiler-cli": "^17.3.0", + "ansi-colors": "^4.1.3", + "esbuild": "^0.25.0", + "semver": "^7.7.1", + "tree-kill": "^1.2.2", + "typescript": "^5.4.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1703.17", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.17.tgz", + "integrity": "sha512-LD6po8lGP2FI7WbnsSxtvpiIi+FYL0aNfteunkT+7po9jUNflBEYHA64UWNO56u7ryKNdbuiN8/TEh7FEUnmCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.17", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/build-angular": { + "version": "17.3.17", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.17.tgz", + "integrity": "sha512-0kLVwjLZ5v4uIaG0K6sHJxxppS0bvjNmxHkbybU8FBW3r5MOBQh/ApsiCQKQQ8GBrQz9qSJvLJH8lsb/uR8aPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1703.17", + "@angular-devkit/build-webpack": "0.1703.17", + "@angular-devkit/core": "17.3.17", + "@babel/core": "7.26.10", + "@babel/generator": "7.26.10", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-transform-async-generator-functions": "7.26.8", + "@babel/plugin-transform-async-to-generator": "7.25.9", + "@babel/plugin-transform-runtime": "7.26.10", + "@babel/preset-env": "7.26.9", + "@babel/runtime": "7.26.10", + "@discoveryjs/json-ext": "0.5.7", + "@ngtools/webpack": "17.3.17", + "@vitejs/plugin-basic-ssl": "1.1.0", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.18", + "babel-loader": "9.1.3", + "babel-plugin-istanbul": "6.1.1", + "browserslist": "^4.21.5", + "copy-webpack-plugin": "11.0.0", + "critters": "0.0.22", + "css-loader": "6.10.0", + "esbuild-wasm": "0.20.1", + "fast-glob": "3.3.2", + "http-proxy-middleware": "2.0.8", + "https-proxy-agent": "7.0.4", + "inquirer": "9.2.15", + "jsonc-parser": "3.2.1", + "karma-source-map-support": "1.4.0", + "less": "4.2.0", + "less-loader": "11.1.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.1", + "magic-string": "0.30.8", + "mini-css-extract-plugin": "2.8.1", + "mrmime": "2.0.0", + "open": "8.4.2", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.1", + "piscina": "4.4.0", + "postcss": "8.4.35", + "postcss-loader": "8.1.1", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.71.1", + "sass-loader": "14.1.1", + "semver": "7.6.0", + "source-map-loader": "5.0.0", + "source-map-support": "0.5.21", + "terser": "5.29.1", + "tree-kill": "1.2.2", + "tslib": "2.6.2", + "vite": "~5.4.17", + "watchpack": "2.4.0", + "webpack": "5.94.0", + "webpack-dev-middleware": "6.1.2", + "webpack-dev-server": "4.15.1", + "webpack-merge": "5.10.0", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.20.1" + }, + "peerDependencies": { + "@angular/compiler-cli": "^17.0.0", + "@angular/localize": "^17.0.0", + "@angular/platform-server": "^17.0.0", + "@angular/service-worker": "^17.0.0", + "@web/test-runner": "^0.18.0", + "browser-sync": "^3.0.2", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "karma": "^6.3.0", + "ng-packagr": "^17.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.2 <5.5" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@web/test-runner": { + "optional": true + }, + "browser-sync": { + "optional": true + }, + "jest": { + "optional": true + }, + "jest-environment-jsdom": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/aix-ppc64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", + "integrity": "sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-arm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.1.tgz", + "integrity": "sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.1.tgz", + "integrity": "sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.1.tgz", + "integrity": "sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/darwin-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.1.tgz", + "integrity": "sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/darwin-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.1.tgz", + "integrity": "sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.1.tgz", + "integrity": "sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/freebsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.1.tgz", + "integrity": "sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-arm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.1.tgz", + "integrity": "sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.1.tgz", + "integrity": "sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-ia32": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.1.tgz", + "integrity": "sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-loong64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.1.tgz", + "integrity": "sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-mips64el": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.1.tgz", + "integrity": "sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-ppc64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.1.tgz", + "integrity": "sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-riscv64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.1.tgz", + "integrity": "sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-s390x": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.1.tgz", + "integrity": "sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz", + "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/netbsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.1.tgz", + "integrity": "sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/openbsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.1.tgz", + "integrity": "sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/sunos-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.1.tgz", + "integrity": "sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.1.tgz", + "integrity": "sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-ia32": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.1.tgz", + "integrity": "sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz", + "integrity": "sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/esbuild": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.1.tgz", + "integrity": "sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.1", + "@esbuild/android-arm": "0.20.1", + "@esbuild/android-arm64": "0.20.1", + "@esbuild/android-x64": "0.20.1", + "@esbuild/darwin-arm64": "0.20.1", + "@esbuild/darwin-x64": "0.20.1", + "@esbuild/freebsd-arm64": "0.20.1", + "@esbuild/freebsd-x64": "0.20.1", + "@esbuild/linux-arm": "0.20.1", + "@esbuild/linux-arm64": "0.20.1", + "@esbuild/linux-ia32": "0.20.1", + "@esbuild/linux-loong64": "0.20.1", + "@esbuild/linux-mips64el": "0.20.1", + "@esbuild/linux-ppc64": "0.20.1", + "@esbuild/linux-riscv64": "0.20.1", + "@esbuild/linux-s390x": "0.20.1", + "@esbuild/linux-x64": "0.20.1", + "@esbuild/netbsd-x64": "0.20.1", + "@esbuild/openbsd-x64": "0.20.1", + "@esbuild/sunos-x64": "0.20.1", + "@esbuild/win32-arm64": "0.20.1", + "@esbuild/win32-ia32": "0.20.1", + "@esbuild/win32-x64": "0.20.1" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/build-angular/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1703.17", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.17.tgz", + "integrity": "sha512-81RJe/WFQ1QOJA9du+jK41KaaWXmEWt3frtj9eseWSr+d+Ebt0JMblzM12A70qm7LoUvG48hSiimm7GmkzV3rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1703.17", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^4.0.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "17.3.17", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.17.tgz", + "integrity": "sha512-7aNVqS3rOGsSZYAOO44xl2KURwaoOP+EJhJs+LqOGOFpok2kd8YLf4CAMUossMF4H7HsJpgKwYqGrV5eXunrpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "17.3.17", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.17.tgz", + "integrity": "sha512-ZXsIJXZm0I0dNu1BqmjfEtQhnzqoupUHHZb4GHm5NeQHBFZctQlkkNxLUU27GVeBUwFgEmP7kFgSLlMPTGSL5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.17", + "jsonc-parser": "3.2.1", + "magic-string": "0.30.8", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular/animations": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.12.tgz", + "integrity": "sha512-9hsdWF4gRRcVJtPcCcYLaX1CIyM9wUu6r+xRl6zU5hq8qhl35hig6ounz7CXFAzLf0WDBdM16bPHouVGaG76lg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "17.3.12" + } + }, + "node_modules/@angular/cli": { + "version": "17.3.17", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.17.tgz", + "integrity": "sha512-FgOvf9q5d23Cpa7cjP1FYti/v8S1FTm8DEkW3TY8lkkoxh3isu28GFKcLD1p/XF3yqfPkPVHToOFla5QwsEgBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1703.17", + "@angular-devkit/core": "17.3.17", + "@angular-devkit/schematics": "17.3.17", + "@schematics/angular": "17.3.17", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.3", + "ini": "4.1.2", + "inquirer": "9.2.15", + "jsonc-parser": "3.2.1", + "npm-package-arg": "11.0.1", + "npm-pick-manifest": "9.0.0", + "open": "8.4.2", + "ora": "5.4.1", + "pacote": "17.0.6", + "resolve": "1.22.8", + "semver": "7.6.0", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/cli/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular/cli/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/@angular/common": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.12.tgz", + "integrity": "sha512-vabJzvrx76XXFrm1RJZ6o/CyG32piTB/1sfFfKHdlH1QrmArb8It4gyk9oEjZ1IkAD0HvBWlfWmn+T6Vx3pdUw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "17.3.12", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.12.tgz", + "integrity": "sha512-vwI8oOL/gM+wPnptOVeBbMfZYwzRxQsovojZf+Zol9szl0k3SZ3FycWlxxXZGFu3VIEfrP6pXplTmyODS/Lt1w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "17.3.12" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.12.tgz", + "integrity": "sha512-1F8M7nWfChzurb7obbvuE7mJXlHtY1UG58pcwcomVtpPb+kPavgAO8OEvJHYBMV+bzSxkXt5UIwL9lt9jHUxZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.23.9", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/index.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/compiler": "17.3.12", + "typescript": ">=5.2 <5.5" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.9", + "@babel/parser": "^7.23.9", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/core": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.12.tgz", + "integrity": "sha512-MuFt5yKi161JmauUta4Dh0m8ofwoq6Ino+KoOtkYMBGsSx+A7dSm+DUxxNwdj7+DNyg3LjVGCFgBFnq4g8z06A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.14.0" + } + }, + "node_modules/@angular/forms": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.12.tgz", + "integrity": "sha512-tV6r12Q3yEUlXwpVko4E+XscunTIpPkLbaiDn/MTL3Vxi2LZnsLgHyd/i38HaHN+e/H3B0a1ToSOhV5wf3ay4Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "17.3.12", + "@angular/core": "17.3.12", + "@angular/platform-browser": "17.3.12", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.12.tgz", + "integrity": "sha512-DYY04ptWh/ulMHzd+y52WCE8QnEYGeIiW3hEIFjCN8z0kbIdFdUtEB0IK5vjNL3ejyhUmphcpeT5PYf3YXtqWQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/animations": "17.3.12", + "@angular/common": "17.3.12", + "@angular/core": "17.3.12" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.3.12.tgz", + "integrity": "sha512-DQwV7B2x/DRLRDSisngZRdLqHdYbbrqZv2Hmu4ZbnNYaWPC8qvzgE/0CvY+UkDat3nCcsfwsMnlDeB6TL7/IaA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "17.3.12", + "@angular/compiler": "17.3.12", + "@angular/core": "17.3.12", + "@angular/platform-browser": "17.3.12" + } + }, + "node_modules/@angular/router": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.3.12.tgz", + "integrity": "sha512-dg7PHBSW9fmPKTVzwvHEeHZPZdpnUqW/U7kj8D29HTP9ur8zZnx9QcnbplwPeYb8yYa62JMnZSEel2X4PxdYBg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "17.3.12", + "@angular/core": "17.3.12", + "@angular/platform-browser": "17.3.12", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", + "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.10", + "@babel/types": "^7.26.10", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", + "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz", + "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.26.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", + "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", + "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", + "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", + "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", + "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", + "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", + "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.26.10.tgz", + "integrity": "sha512-NWaL2qG6HRpONTnj4JvDU6th4jYeZOJgu3QhmFTCihib0ermtOJqktA5BduGm3suhhVe9EMP9c9+mfJ/I9slqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", + "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", + "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz", + "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.26.0", + "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.25.9", + "@babel/plugin-transform-async-generator-functions": "^7.26.8", + "@babel/plugin-transform-async-to-generator": "^7.25.9", + "@babel/plugin-transform-block-scoped-functions": "^7.26.5", + "@babel/plugin-transform-block-scoping": "^7.25.9", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-class-static-block": "^7.26.0", + "@babel/plugin-transform-classes": "^7.25.9", + "@babel/plugin-transform-computed-properties": "^7.25.9", + "@babel/plugin-transform-destructuring": "^7.25.9", + "@babel/plugin-transform-dotall-regex": "^7.25.9", + "@babel/plugin-transform-duplicate-keys": "^7.25.9", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-dynamic-import": "^7.25.9", + "@babel/plugin-transform-exponentiation-operator": "^7.26.3", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-for-of": "^7.26.9", + "@babel/plugin-transform-function-name": "^7.25.9", + "@babel/plugin-transform-json-strings": "^7.25.9", + "@babel/plugin-transform-literals": "^7.25.9", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", + "@babel/plugin-transform-member-expression-literals": "^7.25.9", + "@babel/plugin-transform-modules-amd": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.26.3", + "@babel/plugin-transform-modules-systemjs": "^7.25.9", + "@babel/plugin-transform-modules-umd": "^7.25.9", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-new-target": "^7.25.9", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/plugin-transform-object-rest-spread": "^7.25.9", + "@babel/plugin-transform-object-super": "^7.25.9", + "@babel/plugin-transform-optional-catch-binding": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@babel/plugin-transform-private-property-in-object": "^7.25.9", + "@babel/plugin-transform-property-literals": "^7.25.9", + "@babel/plugin-transform-regenerator": "^7.25.9", + "@babel/plugin-transform-regexp-modifiers": "^7.26.0", + "@babel/plugin-transform-reserved-words": "^7.25.9", + "@babel/plugin-transform-shorthand-properties": "^7.25.9", + "@babel/plugin-transform-spread": "^7.25.9", + "@babel/plugin-transform-sticky-regex": "^7.25.9", + "@babel/plugin-transform-template-literals": "^7.26.8", + "@babel/plugin-transform-typeof-symbol": "^7.26.7", + "@babel/plugin-transform-unicode-escapes": "^7.25.9", + "@babel/plugin-transform-unicode-property-regex": "^7.25.9", + "@babel/plugin-transform-unicode-regex": "^7.25.9", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.40.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ljharb/through": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.14.tgz", + "integrity": "sha512-ajBvlKpWucBB17FuQYUShqpqy8GRgYEpJW0vWJbUu1CV9lWyrDCapy0lScU8T8Z6qn49sSwJB3+M+evYIdGg+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@ngtools/webpack": { + "version": "17.3.17", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.17.tgz", + "integrity": "sha512-LaO++U8DoqV36M0YLKhubc1+NqM8fyp5DN03k1uP9GvtRchP9+7bfG+IEEZiDFkCUh9lfzi1CiGvUHrN4MYcsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^17.0.0", + "typescript": ">=5.2 <5.5", + "webpack": "^5.54.0" + } + }, + "node_modules/@ngx-translate/core": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-15.0.0.tgz", + "integrity": "sha512-Am5uiuR0bOOxyoercDnAA3rJVizo4RRqJHo8N3RqJ+XfzVP/I845yEnMADykOHvM6HkVm4SZSnJBOiz0Anx5BA==", + "license": "SEE LICENSE IN LICENSE", + "engines": { + "node": "^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "rxjs": "^6.5.5 || ^7.4.0" + } + }, + "node_modules/@ngx-translate/http-loader": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-8.0.0.tgz", + "integrity": "sha512-SFMsdUcmHF5OdZkL1CHEoSAwbP5EbAOPTLLboOCRRoOg21P4GJx+51jxGdJeGve6LSKLf4Pay7BkTwmE6vxYlg==", + "license": "SEE LICENSE IN LICENSE", + "engines": { + "node": "^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0", + "@ngx-translate/core": ">=15.0.0", + "rxjs": "^6.5.5 || ^7.4.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.8.tgz", + "integrity": "sha512-liASfw5cqhjNW9UFd+ruwwdEf/lbOAQjLL2XY2dFW/bkJheXDYZgOyul/4gVvEV4BWkTXjYGmDqMw9uegdbJNQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^7.0.0", + "ini": "^4.1.3", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^9.0.0", + "proc-log": "^4.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/git/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", + "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.2.1.tgz", + "integrity": "sha512-f7zYC6kQautXHvNbLEWgD/uGu1+xCn9izgqBfgItWSx22U0ZDekxN08A1vM8cTxj/cRVe0Q94Ode+tdoYmIOOQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^7.0.0", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "proc-log": "^4.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.2.tgz", + "integrity": "sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-1.1.0.tgz", + "integrity": "sha512-PfnWuOkQgu7gCbnSsAisaX7hKOdZ4wSAhAzH3/ph5dSGau52kCRrMMGbiSQLwyTZpgldkZ49b0brkOr1AzGBHQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.4.tgz", + "integrity": "sha512-9ApYM/3+rBt9V80aYg6tZfzj3UWdiYyCt7gJUD1VJKvWF5nwKDSICXbYIQbspFTq6TOpbsEtIC0LArB8d9PFmg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.0.0", + "@npmcli/promise-spawn": "^7.0.0", + "node-gyp": "^10.0.0", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@schematics/angular": { + "version": "17.3.17", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.3.17.tgz", + "integrity": "sha512-S5HwYem5Yjeceb5OLvforNcjfTMh2qsHnTP1BAYL81XPpqeg2udjAkJjKBxCwxMZSqdCMw3ne0eKppEYTaEZ+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.17", + "@angular-devkit/schematics": "17.3.17", + "jsonc-parser": "3.2.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sigstore/bundle": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.3.2.tgz", + "integrity": "sha512-wueKWDk70QixNLB363yHc2D2ItTgYiMTdPwK8D9dKQMR3ZQ0c35IxP5xnwQ8cNLoCgCRcHf14kE+CLIvNX1zmA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-1.1.0.tgz", + "integrity": "sha512-JzBqdVIyqm2FRQCulY6nbQzMpJJpSiJ8XXWMhtOX9eKgaXXpfNOF53lzQEjIydlStnd/eFtuC1dW4VYdD93oRg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.3.tgz", + "integrity": "sha512-RpacQhBlwpBWd7KEJsRKcBQalbV28fvkxwTOJIqhIuDysMMaJW47V4OqW30iJB9uRpqOSxxEAQFdr8tTattReQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.3.2.tgz", + "integrity": "sha512-5Vz5dPVuunIIvC5vBb0APwo7qKA4G9yM48kPWJT+OEERs40md5GoUR1yedwpekWZ4m0Hhw44m6zU+ObsON+iDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "make-fetch-happen": "^13.0.1", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/sign/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.4.tgz", + "integrity": "sha512-44vtsveTPUpqhm9NCrbU8CWLe3Vck2HO1PNLw7RIajbB7xhtn5RBPm1VNSCMwqGYHhDsBJG8gDF0q4lgydsJvw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2", + "tuf-js": "^2.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.2.1.tgz", + "integrity": "sha512-8iKx79/F73DKbGfRf7+t4dqrc0bRr0thdPrxAtCKWRm/F0tG71i6O1rvlnScncJLLBZHn3h8M3c1BSUAb9yu8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.1.0", + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.1.tgz", + "integrity": "sha512-92F7/SFyufn4DXsha9+QfKnN03JGqtMFMXgSHbZOo8JG59WkTni7UzAouNQDf7AuP9OAMxVOPQcqG3sB7w+kkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz", + "integrity": "sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.6.0" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.18", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", + "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001591", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", + "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz", + "integrity": "sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", + "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true, + "license": "ISC" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/critters": { + "version": "0.0.22", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.22.tgz", + "integrity": "sha512-NU7DEcQZM2Dy8XTKFHxtdnIM/drE312j2T4PCVaSUcS0oBeyT/NImpRw/Ap0zOr/1SE7SgPK9tGPg1WK/sVakw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^5.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.2", + "htmlparser2": "^8.0.2", + "postcss": "^8.4.23", + "postcss-media-query-parser": "^0.2.3" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz", + "integrity": "sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.4", + "postcss-modules-scope": "^3.1.1", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.335", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz", + "integrity": "sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.20.1.tgz", + "integrity": "sha512-6v/WJubRsjxBbQdz6izgvx7LsVFvVaGmSdwrFHmEzoVgfXL89hkKPoQHsnVI2ngOkcBUQT9kmAM1hVL1k/Av4A==", + "dev": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.8.tgz", + "integrity": "sha512-/iazaeFPmL8KLA6QB7DFAU4O5j+9y/TA0D019MbLtPuFI56VK4BXFzM6j6QS9oGpScy8IIDH4S2LHv3zg/63Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", + "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.8.tgz", + "integrity": "sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "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.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.2.tgz", + "integrity": "sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/inquirer": { + "version": "9.2.15", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz", + "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ljharb/through": "^2.3.12", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", + "cli-cursor": "^3.1.0", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "figures": "^3.2.0", + "lodash": "^4.17.21", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/launch-editor": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.2.tgz", + "integrity": "sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" + } + }, + "node_modules/less": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", + "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.1.0.tgz", + "integrity": "sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug==", + "dev": true, + "license": "MIT", + "dependencies": { + "klona": "^2.0.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "license": "ISC", + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/make-fetch-happen": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz", + "integrity": "sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-json-stream": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.2.tgz", + "integrity": "sha512-myxeeTm57lYs8pH2nxPzmEEg8DGIgW+9mv6D4JZD2pa81I/OBjeU7PtICXV6c9eRGTA5JMDsuIPUZRCyBMYNhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/minipass-json-stream/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-json-stream/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/needle": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.5.0.tgz", + "integrity": "sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.3.1.tgz", + "integrity": "sha512-Pp3nFHBThHzVtNY7U6JfPjvT/DTE8+o/4xKsLQtBoU+j2HLsGlhcfzflAoUreaJbNmYnX+LlLi0qjV8kpyO6xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^4.1.0", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^4.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", + "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.1.tgz", + "integrity": "sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-packlist": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", + "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^6.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.0.tgz", + "integrity": "sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-16.2.1.tgz", + "integrity": "sha512-8l+7jxhim55S85fjiDGJ1rZXBWGtRLi1OSb4Z3BPLObPuIaeKRlPRiYMSHU4/81ck3t71Z+UwDDl47gcpmfQQA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^1.1.0", + "make-fetch-happen": "^13.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^11.0.0", + "proc-log": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pacote": { + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-17.0.6.tgz", + "integrity": "sha512-cJKrW21VRE8vVTRskJo78c/RCvwJCn1f4qgfxL4w77SOWrTCRcmfkYHlHtS0gqpgjv3zhXflRtgsrUCX5xwNnQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^7.0.0", + "@npmcli/run-script": "^7.0.0", + "cacache": "^18.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^11.0.0", + "npm-packlist": "^8.0.0", + "npm-pick-manifest": "^9.0.0", + "npm-registry-fetch": "^16.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^7.0.0", + "read-package-json-fast": "^3.0.0", + "sigstore": "^2.2.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/piscina": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.4.0.tgz", + "integrity": "sha512-+AQduEJefrOApE4bV7KRmp3N2JnnyErlVqq4P/jmko4FPz9Z877BCccl/iB3FdrWSUkvbGV9Kan/KllJgat3Vg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/postcss": { + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-loader": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^9.0.0", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/primeicons": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz", + "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==", + "license": "MIT" + }, + "node_modules/primeng": { + "version": "17.18.15", + "resolved": "https://registry.npmjs.org/primeng/-/primeng-17.18.15.tgz", + "integrity": "sha512-66iKLPBxuZguebSylKbAst5V3Qz+2dbzT+oCHQnCbv4Gu4JH6WqbBJWr283HacQB1mUNGvyxgcHVVPhQbnEXvA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^17.0.0 || ^18.0.0", + "@angular/core": "^17.0.0 || ^18.0.0", + "@angular/forms": "^17.0.0 || ^18.0.0", + "rxjs": "^6.0.0 || ^7.8.1", + "zone.js": "~0.14.0" + } + }, + "node_modules/proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/read-package-json": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-7.0.1.tgz", + "integrity": "sha512-8PcDiZ8DXUjLf687Ol4BR8Bpm2umR7vhoZOzNRt+uxD9GpBh/K+CAAALVIiYFknmvlmyg7hM7BSNUXPaCCqd0Q==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/regex-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz", + "integrity": "sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz", + "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.71.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz", + "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-loader": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.1.tgz", + "integrity": "sha512-QX8AasDg75monlybel38BZ49JP5Z+uSKfKwF2rO7S74BywaRmGQMUBw9dtkS+ekyM/QnP+NOrRYq8ABMZ9G8jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.8.0", + "mime-types": "~2.1.35", + "parseurl": "~1.3.3" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.3.1.tgz", + "integrity": "sha512-8G+/XDU8wNsJOQS5ysDVO0Etg9/2uA5gR9l4ZwijjlwxBcrU6RPfwi2+jJmbP+Ap1Hlp/nVAaEO4Fj22/SL2gQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "@sigstore/sign": "^2.3.2", + "@sigstore/tuf": "^2.3.4", + "@sigstore/verify": "^1.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/terser": { + "version": "5.29.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz", + "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/terser": { + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tuf-js": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.1.tgz", + "integrity": "sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "2.0.1", + "debug": "^4.3.4", + "make-fetch-happen": "^13.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webpack": { + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz", + "integrity": "sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.12", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", + "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack/node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "license": "MIT", + "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 + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zone.js": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.10.tgz", + "integrity": "sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ==", + "license": "MIT" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..5ef412d --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,38 @@ +{ + "name": "routeros-backup-manager-next-ui", + "version": "1.0.0", + "private": true, + "scripts": { + "start": "ng serve --host 127.0.0.1 --port 4200 --proxy-config proxy.conf.json", + "start:lan": "ng serve --host 0.0.0.0 --port 4200 --proxy-config proxy.conf.json", + "build": "ng build", + "test": "ng test" + }, + "dependencies": { + "@angular/animations": "^17.3.0", + "@angular/common": "^17.3.0", + "@angular/compiler": "^17.3.0", + "@angular/core": "^17.3.0", + "@angular/forms": "^17.3.0", + "@angular/platform-browser": "^17.3.0", + "@angular/platform-browser-dynamic": "^17.3.0", + "@angular/router": "^17.3.0", + "@ngx-translate/core": "^15.0.0", + "@ngx-translate/http-loader": "^8.0.0", + "primeicons": "^7.0.0", + "primeng": "^17.18.0", + "rxjs": "^7.8.1", + "tslib": "^2.6.2", + "zone.js": "^0.14.4" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^17.3.0", + "@angular/cli": "^17.3.0", + "@angular/compiler-cli": "^17.3.0", + "typescript": "^5.4.0", + "ansi-colors": "^4.1.3", + "esbuild": "^0.25.0", + "semver": "^7.7.1", + "tree-kill": "^1.2.2" + } +} diff --git a/frontend/proxy.conf.json b/frontend/proxy.conf.json new file mode 100644 index 0000000..c5ba102 --- /dev/null +++ b/frontend/proxy.conf.json @@ -0,0 +1,8 @@ +{ + "/api": { + "target": "http://127.0.0.1:8000", + "secure": false, + "changeOrigin": true, + "logLevel": "info" + } +} diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html new file mode 100644 index 0000000..4b9b417 --- /dev/null +++ b/frontend/src/app/app.component.html @@ -0,0 +1,68 @@ + + + +
+
+ {{ 'footer.apiOfflineTitle' | translate }} + {{ 'footer.apiOfflineMessage' | translate }} +
+ +
+ + +
+
+ + + +
+ + +
+ +
+ +
+ + {{ 'footer.apiLabel' | translate }}: {{ apiStateLabelKey() | translate }} + {{ 'footer.apiLatencyLabel' | translate }}: {{ apiLatencyLabel() }} + {{ 'footer.apiDocs' | translate }} +
+
+
+
+ + +
+
+ +
+ +
+ + {{ 'footer.apiLabel' | translate }}: {{ apiStateLabelKey() | translate }} + {{ 'footer.apiLatencyLabel' | translate }}: {{ apiLatencyLabel() }} + {{ 'footer.apiDocs' | translate }} +
+
+
diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts new file mode 100644 index 0000000..3a65368 --- /dev/null +++ b/frontend/src/app/app.component.ts @@ -0,0 +1,146 @@ +import { CommonModule } from '@angular/common'; +import { Component, computed, inject } from '@angular/core'; +import { NavigationEnd, Router, RouterOutlet } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { filter } from 'rxjs'; + +import { AuthService } from './core/services/auth.service'; +import { FontService } from './core/services/font.service'; +import { LayoutService } from './core/services/layout.service'; +import { ThemeService } from './core/services/theme.service'; +import { APP_LANGUAGE_OPTIONS, AppLanguage, LanguageService } from './core/services/language.service'; +import { ApiStatusService } from './core/services/api-status.service'; +import { ToastModule } from 'primeng/toast'; +import { ConfirmDialogModule } from 'primeng/confirmdialog'; + +import { AppSidebarComponent } from './shared/layout/app-sidebar.component'; +import { AppTopbarComponent, TopbarLanguageOption } from './shared/layout/app-topbar.component'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [CommonModule, RouterOutlet, TranslateModule, ToastModule, ConfirmDialogModule, AppSidebarComponent, AppTopbarComponent], + templateUrl: './app.component.html' +}) +export class AppComponent { + auth = inject(AuthService); + theme = inject(ThemeService); + language = inject(LanguageService); + font = inject(FontService); + router = inject(Router); + layout = inject(LayoutService); + apiStatus = inject(ApiStatusService); + pageLabel = 'dashboard.title'; + readonly author = 'Mateusz Gruszczyński'; + readonly authorHandle = '@linuxiarz.pl'; + readonly authorUrl = 'https://linuxiarz.pl'; + readonly apiSnapshot = this.apiStatus.snapshot; + readonly languageOptions: TopbarLanguageOption[] = APP_LANGUAGE_OPTIONS; + readonly apiStateLabelKey = computed(() => { + switch (this.apiSnapshot().state) { + case 'online': + return 'footer.apiOnline'; + case 'offline': + return 'footer.apiOffline'; + default: + return 'footer.apiChecking'; + } + }); + readonly apiLatencyLabel = computed(() => { + const latency = this.apiSnapshot().latencyMs; + return latency === null ? '—' : `${latency} ms`; + }); + + readonly menuItems = [ + { label: 'nav.dashboard', link: '/', icon: 'pi pi-home', exact: true }, + { label: 'nav.routers', link: '/routers', icon: 'pi pi-server', exact: false }, + { label: 'nav.files', link: '/files', icon: 'pi pi-folder-open', exact: false }, + { label: 'nav.diffConfigs', link: '/diff-configs', icon: 'pi pi-code', exact: false }, + { label: 'nav.settings', link: '/settings', icon: 'pi pi-cog', exact: false }, + { label: 'nav.logs', link: '/logs', icon: 'pi pi-history', exact: false }, + { label: 'nav.switchosBeta', link: '/switchos-beta', icon: 'pi pi-sitemap', exact: false }, + { label: 'nav.changePassword', link: '/change-password', icon: 'pi pi-lock', exact: false } + ]; + + get currentPageTitle(): string { + return this.pageLabel; + } + + constructor() { + this.language.init(); + this.font.init(); + this.theme.init(); + this.apiStatus.startMonitoring(); + this.auth.restoreSession(); + this.updatePageLabel(this.router.url); + this.router.events.pipe(filter((event) => event instanceof NavigationEnd)).subscribe((event) => { + this.updatePageLabel((event as NavigationEnd).urlAfterRedirects); + this.layout.closeMobileSidebar(); + }); + } + + toggleTheme() { + this.theme.toggle(); + } + + changeLanguage(lang: string) { + const nextLanguage = lang as AppLanguage; + const user = this.auth.user(); + if (!user) { + this.language.set(nextLanguage); + return; + } + + this.language.setForAuthenticatedUser(nextLanguage); + this.auth.updatePreferences({ preferred_language: nextLanguage, preferred_font: user.preferred_font }).subscribe(); + } + + logout() { + this.auth.logout(); + this.router.navigate(['/login']); + } + + refreshApiStatus() { + this.apiStatus.probe(); + } + + get apiStatusClass(): string { + return `layout-footer__status--${this.apiSnapshot().state}`; + } + + private updatePageLabel(url: string) { + if (url.startsWith('/routers/')) { + this.pageLabel = 'routers.detailTitle'; + return; + } + if (url.startsWith('/routers')) { + this.pageLabel = 'routers.title'; + return; + } + if (url.startsWith('/files')) { + this.pageLabel = 'files.title'; + return; + } + if (url.startsWith('/diff-configs')) { + this.pageLabel = 'diffConfigs.title'; + return; + } + if (url.startsWith('/settings')) { + this.pageLabel = 'settings.title'; + return; + } + if (url.startsWith('/logs')) { + this.pageLabel = 'logs.title'; + return; + } + if (url.startsWith('/switchos-beta')) { + this.pageLabel = 'switchosBeta.title'; + return; + } + if (url.startsWith('/change-password')) { + this.pageLabel = 'auth.changePassword'; + return; + } + this.pageLabel = 'dashboard.title'; + } +} diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts new file mode 100644 index 0000000..f427a65 --- /dev/null +++ b/frontend/src/app/app.routes.ts @@ -0,0 +1,29 @@ +import { Routes } from '@angular/router'; + +import { authGuard } from './core/guards/auth.guard'; +import { ChangePasswordPageComponent } from './features/auth/change-password-page.component'; +import { LoginPageComponent } from './features/auth/login-page.component'; +import { RegisterPageComponent } from './features/auth/register-page.component'; +import { DashboardPageComponent } from './features/dashboard/dashboard-page.component'; +import { DiffConfigsPageComponent } from './features/diff-configs/diff-configs-page.component'; +import { FilesPageComponent } from './features/files/files-page.component'; +import { LogsPageComponent } from './features/logs/logs-page.component'; +import { RouterDetailPageComponent } from './features/routers/router-detail-page.component'; +import { RoutersPageComponent } from './features/routers/routers-page.component'; +import { SettingsPageComponent } from './features/settings/settings-page.component'; +import { SwosBetaPageComponent } from './features/swos-beta/swos-beta-page.component'; + +export const routes: Routes = [ + { path: 'login', component: LoginPageComponent }, + { path: 'register', component: RegisterPageComponent }, + { path: 'change-password', canActivate: [authGuard], component: ChangePasswordPageComponent }, + { path: '', canActivate: [authGuard], component: DashboardPageComponent }, + { path: 'routers', canActivate: [authGuard], component: RoutersPageComponent }, + { path: 'routers/:id', canActivate: [authGuard], component: RouterDetailPageComponent }, + { path: 'files', canActivate: [authGuard], component: FilesPageComponent }, + { path: 'diff-configs', canActivate: [authGuard], component: DiffConfigsPageComponent }, + { path: 'settings', canActivate: [authGuard], component: SettingsPageComponent }, + { path: 'logs', canActivate: [authGuard], component: LogsPageComponent }, + { path: 'switchos-beta', canActivate: [authGuard], component: SwosBetaPageComponent }, + { path: '**', redirectTo: '' } +]; diff --git a/frontend/src/app/core/guards/auth.guard.ts b/frontend/src/app/core/guards/auth.guard.ts new file mode 100644 index 0000000..254777b --- /dev/null +++ b/frontend/src/app/core/guards/auth.guard.ts @@ -0,0 +1,14 @@ +import { inject } from '@angular/core'; +import { Router } from '@angular/router'; + +import { AuthService } from '../services/auth.service'; + +export const authGuard = () => { + const auth = inject(AuthService); + const router = inject(Router); + if (!auth.isLoggedIn()) { + router.navigate(['/login']); + return false; + } + return true; +}; diff --git a/frontend/src/app/core/guards/guest.guard.ts b/frontend/src/app/core/guards/guest.guard.ts new file mode 100644 index 0000000..438502d --- /dev/null +++ b/frontend/src/app/core/guards/guest.guard.ts @@ -0,0 +1,15 @@ +import { inject } from '@angular/core'; +import { Router } from '@angular/router'; + +import { AuthService } from '../services/auth.service'; + +export const guestGuard = () => { + const auth = inject(AuthService); + const router = inject(Router); + + if (auth.isLoggedIn()) { + return router.createUrlTree(['/']); + } + + return true; +}; diff --git a/frontend/src/app/core/interceptors/auth.interceptor.ts b/frontend/src/app/core/interceptors/auth.interceptor.ts new file mode 100644 index 0000000..34ba8c3 --- /dev/null +++ b/frontend/src/app/core/interceptors/auth.interceptor.ts @@ -0,0 +1,7 @@ +import { HttpInterceptorFn } from '@angular/common/http'; + +export const authInterceptor: HttpInterceptorFn = (req, next) => { + const token = localStorage.getItem('routeros_token'); + if (!token) return next(req); + return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })); +}; diff --git a/frontend/src/app/core/services/api-status.service.ts b/frontend/src/app/core/services/api-status.service.ts new file mode 100644 index 0000000..3bdae2b --- /dev/null +++ b/frontend/src/app/core/services/api-status.service.ts @@ -0,0 +1,83 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { catchError, fromEvent, interval, merge, of, Subscription, timeout } from 'rxjs'; + +type ApiConnectionState = 'checking' | 'online' | 'offline'; + +interface HealthResponse { + status?: string; + timestamp?: string; +} + +export interface ApiStatusSnapshot { + state: ApiConnectionState; + latencyMs: number | null; + checkedAt: string | null; +} + +@Injectable({ providedIn: 'root' }) +export class ApiStatusService { + private readonly http = inject(HttpClient); + private started = false; + private inFlight = false; + private pollSubscription?: Subscription; + private offlineSubscription?: Subscription; + + readonly snapshot = signal({ + state: 'checking', + latencyMs: null, + checkedAt: null + }); + + startMonitoring() { + if (this.started || typeof window === 'undefined') { + return; + } + this.started = true; + + this.pollSubscription = merge(of(0), interval(15000), fromEvent(window, 'focus'), fromEvent(window, 'online')).subscribe(() => { + this.probe(); + }); + + this.offlineSubscription = fromEvent(window, 'offline').subscribe(() => { + this.snapshot.set({ + state: 'offline', + latencyMs: null, + checkedAt: new Date().toISOString() + }); + }); + } + + probe() { + if (this.inFlight) { + return; + } + this.inFlight = true; + const startedAt = performance.now(); + const params = new HttpParams().set('_', String(Date.now())); + + this.http + .get('/api/health', { params }) + .pipe( + timeout(4000), + catchError(() => of(null)) + ) + .subscribe((response) => { + const checkedAt = response?.timestamp || new Date().toISOString(); + if (response?.status === 'ok') { + this.snapshot.set({ + state: 'online', + latencyMs: Math.max(1, Math.round(performance.now() - startedAt)), + checkedAt + }); + } else { + this.snapshot.set({ + state: 'offline', + latencyMs: null, + checkedAt + }); + } + this.inFlight = false; + }); + } +} diff --git a/frontend/src/app/core/services/api.service.ts b/frontend/src/app/core/services/api.service.ts new file mode 100644 index 0000000..a140054 --- /dev/null +++ b/frontend/src/app/core/services/api.service.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +@Injectable({ providedIn: 'root' }) +export class ApiService { + readonly baseUrl = '/api'; + + constructor(public http: HttpClient) {} +} diff --git a/frontend/src/app/core/services/auth.service.ts b/frontend/src/app/core/services/auth.service.ts new file mode 100644 index 0000000..04ca6a9 --- /dev/null +++ b/frontend/src/app/core/services/auth.service.ts @@ -0,0 +1,88 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { Injectable, inject, signal } from '@angular/core'; +import { tap } from 'rxjs'; + +import { AppFont, FontService } from './font.service'; +import { AppLanguage, LanguageService } from './language.service'; + +export interface AuthUser { + id: number; + username: string; + preferred_language: AppLanguage; + preferred_font: AppFont; +} + +@Injectable({ providedIn: 'root' }) +export class AuthService { + private readonly api = '/api'; + private readonly tokenKey = 'routeros_token'; + private readonly language = inject(LanguageService); + private readonly font = inject(FontService); + + readonly user = signal(null); + + constructor(private http: HttpClient) {} + + login(username: string, password: string) { + const body = new URLSearchParams({ username, password }); + return this.http + .post<{ access_token: string; user: AuthUser }>(`${this.api}/auth/login`, body.toString(), { + headers: new HttpHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' }) + }) + .pipe( + tap((response) => { + localStorage.setItem(this.tokenKey, response.access_token); + this.setUserAndApplyPreferences(response.user); + }) + ); + } + + register(username: string, password: string) { + return this.http.post<{ id: number; username: string }>(`${this.api}/auth/register`, { username, password }); + } + + changePassword(current_password: string, new_password: string) { + return this.http.post<{ message: string }>(`${this.api}/auth/change-password`, { current_password, new_password }); + } + + restoreSession() { + const token = this.token(); + if (!token) { + this.user.set(null); + this.language.resetToGuestPreference(); + return; + } + this.http.get(`${this.api}/auth/me`).subscribe({ + next: (user) => this.setUserAndApplyPreferences(user), + error: () => this.logout() + }); + } + + updatePreferences(preferences: { preferred_language: AppLanguage; preferred_font: AppFont }) { + return this.http.put(`${this.api}/auth/preferences`, preferences).pipe( + tap((user) => { + this.setUserAndApplyPreferences(user); + }) + ); + } + + logout() { + localStorage.removeItem(this.tokenKey); + this.user.set(null); + this.language.resetToGuestPreference(); + } + + token() { + return localStorage.getItem(this.tokenKey); + } + + isLoggedIn() { + return !!this.token(); + } + + private setUserAndApplyPreferences(user: AuthUser) { + this.user.set(user); + this.language.applyForUser(user.preferred_language || 'pl'); + this.font.set(user.preferred_font || 'default'); + } +} diff --git a/frontend/src/app/core/services/font.service.ts b/frontend/src/app/core/services/font.service.ts new file mode 100644 index 0000000..f2727d6 --- /dev/null +++ b/frontend/src/app/core/services/font.service.ts @@ -0,0 +1,56 @@ +import { DOCUMENT } from '@angular/common'; +import { Injectable, computed, inject, signal } from '@angular/core'; + +export type AppFont = 'default' | 'adwaita_mono' | 'hack'; + +@Injectable({ providedIn: 'root' }) +export class FontService { + private readonly key = 'routeros_font'; + private readonly document = inject(DOCUMENT); + private readonly supportedFonts: AppFont[] = ['default', 'adwaita_mono', 'hack']; + private readonly fontState = signal('default'); + + readonly current = computed(() => this.fontState()); + + init() { + const stored = localStorage.getItem(this.key) as AppFont | null; + const font = stored && this.supportedFonts.includes(stored) ? stored : 'default'; + this.set(font); + } + + set(font: AppFont) { + const nextFont = this.supportedFonts.includes(font) ? font : 'default'; + this.fontState.set(nextFont); + localStorage.setItem(this.key, nextFont); + this.applyFont(nextFont); + } + + private applyFont(font: AppFont) { + const root = this.document.documentElement; + const body = this.document.body; + const families = this.fontFamilies(font); + root.style.setProperty('--font-body', families.body); + root.style.setProperty('--font-title', families.title); + body.setAttribute('data-app-font', font); + } + + private fontFamilies(font: AppFont): { body: string; title: string } { + switch (font) { + case 'adwaita_mono': + return { + body: "'Adwaita Mono', 'Roboto Mono', 'IBM Plex Mono', 'SFMono-Regular', Consolas, monospace", + title: "'Adwaita Mono', 'Roboto Mono', 'IBM Plex Mono', 'SFMono-Regular', Consolas, monospace" + }; + case 'hack': + return { + body: "Hack, 'Roboto Mono', 'IBM Plex Mono', 'SFMono-Regular', Consolas, monospace", + title: "Hack, 'Roboto Mono', 'IBM Plex Mono', 'SFMono-Regular', Consolas, monospace" + }; + default: + return { + body: "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", + title: "'Roboto Mono', 'IBM Plex Mono', 'SFMono-Regular', Consolas, monospace" + }; + } + } +} diff --git a/frontend/src/app/core/services/language.service.ts b/frontend/src/app/core/services/language.service.ts new file mode 100644 index 0000000..6bfbd14 --- /dev/null +++ b/frontend/src/app/core/services/language.service.ts @@ -0,0 +1,68 @@ +import { Injectable, computed, inject, signal } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; + +export type AppLanguage = 'pl' | 'en' | 'es' | 'no'; + +export interface AppLanguageOption { + code: AppLanguage; + label: string; + flag: string; +} + +export const APP_LANGUAGE_OPTIONS: AppLanguageOption[] = [ + { code: 'pl', label: 'Polski', flag: '🇵🇱' }, + { code: 'en', label: 'English', flag: '🇬🇧' }, + { code: 'es', label: 'Español', flag: '🇪🇸' }, + { code: 'no', label: 'Norsk', flag: '🇳🇴' } +]; + +@Injectable({ providedIn: 'root' }) +export class LanguageService { + private readonly key = 'routeros_lang'; + private readonly translate = inject(TranslateService); + private readonly supportedLanguages = APP_LANGUAGE_OPTIONS.map((option) => option.code); + private readonly langState = signal('pl'); + + readonly current = computed(() => this.langState()); + readonly options = APP_LANGUAGE_OPTIONS; + + init() { + this.translate.setDefaultLang('pl'); + this.apply(this.guestPreference()); + } + + set(lang: AppLanguage) { + const nextLang = this.read(lang) || 'pl'; + localStorage.setItem(this.key, nextLang); + this.apply(nextLang); + } + + setForAuthenticatedUser(lang: AppLanguage) { + const nextLang = this.read(lang) || 'pl'; + this.apply(nextLang); + } + + applyForUser(preferredLanguage?: AppLanguage | null) { + this.apply(this.read(preferredLanguage || null) || 'pl'); + } + + resetToGuestPreference() { + this.apply(this.guestPreference()); + } + + private guestPreference(): AppLanguage { + return this.read(localStorage.getItem(this.key)) || 'pl'; + } + + private apply(lang: AppLanguage) { + this.langState.set(lang); + this.translate.use(lang); + } + + private read(value: string | null | undefined): AppLanguage | null { + if (!value) { + return null; + } + return this.supportedLanguages.includes(value as AppLanguage) ? (value as AppLanguage) : null; + } +} diff --git a/frontend/src/app/core/services/layout.service.ts b/frontend/src/app/core/services/layout.service.ts new file mode 100644 index 0000000..a1617da --- /dev/null +++ b/frontend/src/app/core/services/layout.service.ts @@ -0,0 +1,22 @@ +import { Injectable, computed, signal } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class LayoutService { + private readonly collapsedState = signal(false); + private readonly mobileOpenState = signal(false); + + readonly collapsed = computed(() => this.collapsedState()); + readonly mobileOpen = computed(() => this.mobileOpenState()); + + toggleSidebar() { + if (typeof window !== 'undefined' && window.innerWidth < 992) { + this.mobileOpenState.update((value) => !value); + return; + } + this.collapsedState.update((value) => !value); + } + + closeMobileSidebar() { + this.mobileOpenState.set(false); + } +} diff --git a/frontend/src/app/core/services/theme.service.ts b/frontend/src/app/core/services/theme.service.ts new file mode 100644 index 0000000..3545ad1 --- /dev/null +++ b/frontend/src/app/core/services/theme.service.ts @@ -0,0 +1,25 @@ +import { Injectable, computed, signal } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class ThemeService { + private readonly key = 'routeros_theme'; + private readonly modeState = signal<'light' | 'dark'>('dark'); + + readonly mode = computed(() => this.modeState()); + readonly isDark = computed(() => this.modeState() === 'dark'); + + init() { + const mode = (localStorage.getItem(this.key) as 'light' | 'dark' | null) || 'dark'; + this.set(mode); + } + + toggle() { + this.set(this.modeState() === 'dark' ? 'light' : 'dark'); + } + + set(mode: 'light' | 'dark') { + this.modeState.set(mode); + document.body.classList.toggle('dark-theme', mode === 'dark'); + localStorage.setItem(this.key, mode); + } +} diff --git a/frontend/src/app/core/services/ui.service.ts b/frontend/src/app/core/services/ui.service.ts new file mode 100644 index 0000000..6582692 --- /dev/null +++ b/frontend/src/app/core/services/ui.service.ts @@ -0,0 +1,81 @@ +import { Injectable, inject } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { ConfirmationService, MessageService } from 'primeng/api'; + +interface ConfirmOptions { + messageKey: string; + params?: Record; + headerKey?: string; + acceptKey?: string; + rejectKey?: string; + acceptButtonStyleClass?: string; +} + +@Injectable({ providedIn: 'root' }) +export class UiService { + private readonly messageService = inject(MessageService); + private readonly confirmationService = inject(ConfirmationService); + private readonly translate = inject(TranslateService); + + success(detailKey: string, params?: Record) { + this.messageService.add({ + severity: 'success', + summary: this.t('toast.success'), + detail: this.t(detailKey, params) + }); + } + + info(detailKey: string, params?: Record) { + this.messageService.add({ + severity: 'info', + summary: this.t('toast.info'), + detail: this.t(detailKey, params) + }); + } + + error(detailKey: string, params?: Record) { + this.messageService.add({ + severity: 'error', + summary: this.t('toast.error'), + detail: this.t(detailKey, params) + }); + } + + clear() { + this.messageService.clear(); + } + + confirm(options: ConfirmOptions): Promise { + return new Promise((resolve) => { + let resolved = false; + const finish = (value: boolean) => { + if (!resolved) { + resolved = true; + resolve(value); + } + }; + + this.confirmationService.confirm({ + header: this.t(options.headerKey ?? 'confirm.header'), + message: this.t(options.messageKey, options.params), + icon: 'pi pi-exclamation-triangle', + acceptLabel: this.t(options.acceptKey ?? 'common.confirm'), + rejectLabel: this.t(options.rejectKey ?? 'common.cancel'), + acceptButtonStyleClass: options.acceptButtonStyleClass ?? 'p-button-danger', + rejectButtonStyleClass: 'p-button-text', + closeOnEscape: true, + dismissableMask: true, + accept: () => finish(true), + reject: () => finish(false) + }); + }); + } + + instant(key: string, params?: Record) { + return this.t(key, params); + } + + private t(key: string, params?: Record): string { + return this.translate.instant(key, params); + } +} diff --git a/frontend/src/app/features/auth/change-password-page.component.html b/frontend/src/app/features/auth/change-password-page.component.html new file mode 100644 index 0000000..cd32840 --- /dev/null +++ b/frontend/src/app/features/auth/change-password-page.component.html @@ -0,0 +1,50 @@ + + +
+ +
+
+ {{ 'auth.passwordStrength' | translate }} + {{ passwordStrengthLabel }} +
+
+
+
+ + {{ 'auth.ruleLength' | translate }} +
+
+ + {{ 'auth.ruleDigit' | translate }} +
+
+ + {{ 'auth.ruleMatch' | translate }} +
+
+
+
+ + +
+ + + + + + + + + + + + + {{ passwordsMatch ? ('auth.passwordsMatchHint' | translate) : ('auth.passwordsMismatch' | translate) }} + {{ error }} + +
+
+
diff --git a/frontend/src/app/features/auth/change-password-page.component.ts b/frontend/src/app/features/auth/change-password-page.component.ts new file mode 100644 index 0000000..29b7de0 --- /dev/null +++ b/frontend/src/app/features/auth/change-password-page.component.ts @@ -0,0 +1,90 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { Router, RouterLink } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { ButtonModule } from 'primeng/button'; +import { InputTextModule } from 'primeng/inputtext'; + +import { AuthService } from '../../core/services/auth.service'; +import { UiService } from '../../core/services/ui.service'; +import { PageHeaderComponent } from '../../shared/ui/page-header.component'; +import { SectionCardComponent } from '../../shared/ui/section-card.component'; + +@Component({ + standalone: true, + imports: [CommonModule, ReactiveFormsModule, RouterLink, TranslateModule, ButtonModule, InputTextModule, PageHeaderComponent, SectionCardComponent], + templateUrl: './change-password-page.component.html' +}) +export class ChangePasswordPageComponent { + private readonly fb = inject(FormBuilder); + private readonly auth = inject(AuthService); + private readonly router = inject(Router); + private readonly ui = inject(UiService); + + error = ''; + submitting = false; + readonly form = this.fb.nonNullable.group({ + current_password: ['', Validators.required], + new_password: ['', [Validators.required, Validators.minLength(4)]], + confirmPassword: ['', [Validators.required, Validators.minLength(4)]] + }); + + + get hasMinLength(): boolean { + return (this.form.controls.new_password.value || '').length >= 8; + } + + get hasDigit(): boolean { + return /[0-9]/.test(this.form.controls.new_password.value || ''); + } + + get passwordStrengthPercent(): number { + const value = this.form.controls.new_password.value || ''; + let score = 0; + if (value.length >= 8) score += 35; + if (/[A-Z]/.test(value)) score += 20; + if (/[a-z]/.test(value)) score += 15; + if (/[0-9]/.test(value)) score += 15; + if (/[^A-Za-z0-9]/.test(value)) score += 15; + return Math.min(100, score); + } + + get passwordStrengthLabel(): string { + const score = this.passwordStrengthPercent; + if (score >= 80) return this.ui.instant('auth.passwordStrong'); + if (score >= 50) return this.ui.instant('auth.passwordMedium'); + return this.ui.instant('auth.passwordWeak'); + } + + get passwordsMatch(): boolean { + const { new_password, confirmPassword } = this.form.getRawValue(); + return !!new_password && new_password === confirmPassword; + } + + submit() { + if (this.form.invalid || this.submitting) { + return; + } + this.error = ''; + const { current_password, new_password, confirmPassword } = this.form.getRawValue(); + if (new_password !== confirmPassword) { + this.error = this.ui.instant('auth.passwordsMismatch'); + return; + } + this.submitting = true; + this.auth.changePassword(current_password, new_password).subscribe({ + next: () => { + this.ui.success('toast.passwordChanged'); + setTimeout(() => this.router.navigate(['/']), 500); + }, + error: (err) => { + this.error = err?.error?.detail ?? this.ui.instant('auth.changePasswordFailed'); + this.submitting = false; + }, + complete: () => { + this.submitting = false; + } + }); + } +} diff --git a/frontend/src/app/features/auth/login-page.component.html b/frontend/src/app/features/auth/login-page.component.html new file mode 100644 index 0000000..92b894c --- /dev/null +++ b/frontend/src/app/features/auth/login-page.component.html @@ -0,0 +1,29 @@ +
+ + +
+
+

{{ 'auth.login' | translate }}

+

{{ 'auth.loginSubtitle' | translate }}

+
+ +
+ + + + + + + + + + + {{ error }} + + +
+
+
diff --git a/frontend/src/app/features/auth/login-page.component.ts b/frontend/src/app/features/auth/login-page.component.ts new file mode 100644 index 0000000..00bdc06 --- /dev/null +++ b/frontend/src/app/features/auth/login-page.component.ts @@ -0,0 +1,49 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { Router, RouterLink } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { ButtonModule } from 'primeng/button'; +import { InputTextModule } from 'primeng/inputtext'; + +import { AuthService } from '../../core/services/auth.service'; +import { UiService } from '../../core/services/ui.service'; +import { AuthToolbarComponent } from '../../shared/auth/auth-toolbar.component'; + +@Component({ + standalone: true, + imports: [CommonModule, ReactiveFormsModule, RouterLink, TranslateModule, ButtonModule, InputTextModule, AuthToolbarComponent], + templateUrl: './login-page.component.html' +}) +export class LoginPageComponent { + private readonly fb = inject(FormBuilder); + private readonly auth = inject(AuthService); + private readonly router = inject(Router); + private readonly ui = inject(UiService); + + error = ''; + submitting = false; + readonly form = this.fb.nonNullable.group({ + username: ['admin', Validators.required], + password: ['admin', Validators.required] + }); + + submit() { + if (this.form.invalid || this.submitting) { + return; + } + this.error = ''; + this.submitting = true; + const { username, password } = this.form.getRawValue(); + this.auth.login(username, password).subscribe({ + next: () => this.router.navigate(['/']), + error: (err) => { + this.error = err?.error?.detail ?? this.ui.instant('auth.loginFailed'); + this.submitting = false; + }, + complete: () => { + this.submitting = false; + } + }); + } +} diff --git a/frontend/src/app/features/auth/register-page.component.html b/frontend/src/app/features/auth/register-page.component.html new file mode 100644 index 0000000..b43502e --- /dev/null +++ b/frontend/src/app/features/auth/register-page.component.html @@ -0,0 +1,28 @@ +
+ +
+
+

{{ 'auth.register' | translate }}

+
+
+ + + + + + + + + + + + + {{ error }} + {{ success }} + +
+
+
diff --git a/frontend/src/app/features/auth/register-page.component.ts b/frontend/src/app/features/auth/register-page.component.ts new file mode 100644 index 0000000..cbe484b --- /dev/null +++ b/frontend/src/app/features/auth/register-page.component.ts @@ -0,0 +1,59 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { Router, RouterLink } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { ButtonModule } from 'primeng/button'; +import { InputTextModule } from 'primeng/inputtext'; + +import { AuthService } from '../../core/services/auth.service'; +import { UiService } from '../../core/services/ui.service'; +import { AuthToolbarComponent } from '../../shared/auth/auth-toolbar.component'; + +@Component({ + standalone: true, + imports: [CommonModule, ReactiveFormsModule, RouterLink, TranslateModule, ButtonModule, InputTextModule, AuthToolbarComponent], + templateUrl: './register-page.component.html' +}) +export class RegisterPageComponent { + private readonly fb = inject(FormBuilder); + private readonly auth = inject(AuthService); + private readonly router = inject(Router); + private readonly ui = inject(UiService); + + error = ''; + success = ''; + submitting = false; + readonly form = this.fb.nonNullable.group({ + username: ['', [Validators.required, Validators.minLength(3)]], + password: ['', [Validators.required, Validators.minLength(4)]], + confirmPassword: ['', [Validators.required, Validators.minLength(4)]] + }); + + submit() { + if (this.form.invalid || this.submitting) { + return; + } + this.error = ''; + this.success = ''; + const { username, password, confirmPassword } = this.form.getRawValue(); + if (password !== confirmPassword) { + this.error = this.ui.instant('auth.passwordsMismatch'); + return; + } + this.submitting = true; + this.auth.register(username, password).subscribe({ + next: () => { + this.success = this.ui.instant('auth.accountCreated'); + setTimeout(() => this.router.navigate(['/login']), 500); + }, + error: (err) => { + this.error = err?.error?.detail ?? this.ui.instant('auth.registrationFailed'); + this.submitting = false; + }, + complete: () => { + this.submitting = false; + } + }); + } +} diff --git a/frontend/src/app/features/dashboard/dashboard-page.component.html b/frontend/src/app/features/dashboard/dashboard-page.component.html new file mode 100644 index 0000000..ad791a5 --- /dev/null +++ b/frontend/src/app/features/dashboard/dashboard-page.component.html @@ -0,0 +1,107 @@ + + +
+ + + + +
+ + +
+
+ + +
+ +
+
+ {{ 'dashboard.latestSnapshot' | translate }} + {{ latestBackupLabel }} + {{ latestBackupHint }} +
+
+ {{ 'dashboard.coverageLabel' | translate }} + {{ coveragePercent }}% + {{ 'dashboard.coverageHint' | translate }} +
+
+ {{ 'dashboard.weeklyActivityLabel' | translate }} + {{ backupsLast7Days }} + {{ 'dashboard.weeklyActivityHint' | translate }} +
+
+ {{ 'dashboard.busiestRouterLabel' | translate }} + {{ busiestRouterLabel }} + {{ busiestRouterHint }} +
+
+
+
+ +
+ +
+
+
+ {{ formatPercent(usedPercent) }} + {{ 'dashboard.diskUsage' | translate }} +
+
+ +
+

{{ 'dashboard.storageSubtitle' | translate }}

+
+
+ {{ 'dashboard.totalDisk' | translate }} + {{ formatBytes(data.storage.total) }} +
+
+ {{ 'dashboard.usedSpace' | translate }} + {{ formatBytes(usedBytes) }} +
+
+ {{ 'dashboard.freeSpace' | translate }} + {{ formatBytes(data.storage.free) }} +
+
+ {{ 'dashboard.folderUsage' | translate }} + {{ formatBytes(data.storage.folder_used) }} +
+
+
+
+
+ + + +
+
+

{{ 'dashboard.weeklyActivityHint' | translate }}

+ {{ backupsLast7Days }} +
+ +
+
+ {{ item.label }} +
+ +
+ {{ item.value }} +
+
+
+
+
+
+ + +
+ +

{{ 'dashboard.noActivity' | translate }}

+
+
diff --git a/frontend/src/app/features/dashboard/dashboard-page.component.ts b/frontend/src/app/features/dashboard/dashboard-page.component.ts new file mode 100644 index 0000000..ef6d6c3 --- /dev/null +++ b/frontend/src/app/features/dashboard/dashboard-page.component.ts @@ -0,0 +1,413 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnInit, inject } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { ButtonModule } from 'primeng/button'; +import { forkJoin } from 'rxjs'; + +import { ApiService } from '../../core/services/api.service'; +import { UiService } from '../../core/services/ui.service'; +import { PageHeaderComponent } from '../../shared/ui/page-header.component'; +import { SectionCardComponent } from '../../shared/ui/section-card.component'; +import { StatCardComponent } from '../../shared/ui/stat-card.component'; + +interface DashboardData { + routers_count: number; + export_count: number; + binary_count: number; + total_backups: number; + recent_logs: { timestamp: string; message: string }[]; + storage: { total: number; used: number; free: number; folder_used: number; usage_percent: number }; +} + +interface BackupInventoryItem { + id: number; + router_id: number; + router_name?: string; + backup_type: 'export' | 'binary'; + created_at: string; + file_size?: number | null; +} + +interface RouterInventoryItem { + id: number; + name: string; +} + +type ChartTone = 'accent' | 'success' | 'info' | 'warning'; + +interface StorageChartRow { + labelKey: string; + value: string; + percent: number; + tone: ChartTone; +} + +interface StorageActivityBar { + label: string; + fullLabel: string; + value: number; + height: number; +} + +interface RouterBackupBar { + label: string; + value: number; + percent: number; +} + +@Component({ + standalone: true, + imports: [CommonModule, TranslateModule, ButtonModule, PageHeaderComponent, SectionCardComponent, StatCardComponent], + templateUrl: './dashboard-page.component.html' +}) +export class DashboardPageComponent implements OnInit { + private readonly api = inject(ApiService); + private readonly ui = inject(UiService); + + data?: DashboardData; + backups: BackupInventoryItem[] = []; + routers: RouterInventoryItem[] = []; + exporting = false; + runningBinary = false; + readonly activityPageSize = 6; + activityPage = 0; + + ngOnInit() { + this.load(); + } + + load() { + forkJoin({ + dashboard: this.api.http.get(`${this.api.baseUrl}/dashboard`), + backups: this.api.http.get(`${this.api.baseUrl}/backups`), + routers: this.api.http.get(`${this.api.baseUrl}/routers`) + }).subscribe(({ dashboard, backups, routers }) => { + this.data = dashboard; + this.backups = backups; + this.routers = routers; + this.activityPage = 0; + }); + } + + exportAll() { + if (this.exporting) { + return; + } + this.exporting = true; + this.api.http.post(`${this.api.baseUrl}/backups/routers/export-all`, {}).subscribe({ + next: (result) => { + this.ui.success('toast.exportedRouters', { count: result.filter((item) => item.status === 'ok').length }); + this.load(); + }, + complete: () => { + this.exporting = false; + } + }); + } + + binaryAll() { + if (this.runningBinary) { + return; + } + this.runningBinary = true; + this.api.http.post(`${this.api.baseUrl}/backups/routers/binary-all`, {}).subscribe({ + next: (result) => { + this.ui.success('toast.binaryCompletedRouters', { count: result.filter((item) => item.status === 'ok').length }); + this.load(); + }, + complete: () => { + this.runningBinary = false; + } + }); + } + + get usedBytes(): number { + const storage = this.data?.storage; + if (!storage) { + return 0; + } + const computed = Math.max(0, Number(storage.total || 0) - Number(storage.free || 0)); + if (computed > 0) { + return computed; + } + return Math.max(0, Number(storage.used || 0)); + } + + get usedPercent(): number { + const total = Number(this.data?.storage.total || 0); + return total > 0 ? (this.usedBytes / total) * 100 : 0; + } + + get freePercent(): number { + const storage = this.data?.storage; + const total = Number(storage?.total || 0); + const free = Math.max(0, Number(storage?.free || 0)); + return total > 0 ? (free / total) * 100 : 0; + } + + get folderPercent(): number { + const storage = this.data?.storage; + const total = Number(storage?.total || 0); + const folderUsed = Math.max(0, Number(storage?.folder_used || 0)); + return total > 0 ? (folderUsed / total) * 100 : 0; + } + + get storageCapacityRows(): StorageChartRow[] { + const storage = this.data?.storage; + if (!storage) { + return []; + } + return [ + { labelKey: 'dashboard.totalDisk', value: this.formatBytes(storage.total), percent: 100, tone: 'accent' }, + { labelKey: 'dashboard.usedSpace', value: this.formatBytes(this.usedBytes), percent: this.usedPercent, tone: 'warning' }, + { labelKey: 'dashboard.freeSpace', value: this.formatBytes(storage.free), percent: this.freePercent, tone: 'success' }, + { labelKey: 'dashboard.folderUsage', value: this.formatBytes(storage.folder_used), percent: this.folderPercent, tone: 'info' } + ]; + } + + get backupMixRows(): StorageChartRow[] { + const exportCount = Number(this.data?.export_count || 0); + const binaryCount = Number(this.data?.binary_count || 0); + const total = exportCount + binaryCount; + return [ + { + labelKey: 'dashboard.exportsCard', + value: String(exportCount), + percent: total > 0 ? (exportCount / total) * 100 : 0, + tone: 'accent' + }, + { + labelKey: 'dashboard.binaryCard', + value: String(binaryCount), + percent: total > 0 ? (binaryCount / total) * 100 : 0, + tone: 'warning' + } + ]; + } + + get backupActivityRows(): StorageActivityBar[] { + if (!this.backups.length) { + return []; + } + + const today = new Date(); + today.setHours(0, 0, 0, 0); + const counts = new Map(); + + for (const backup of this.backups) { + const value = new Date(backup.created_at); + value.setHours(0, 0, 0, 0); + const key = value.toISOString().slice(0, 10); + counts.set(key, (counts.get(key) || 0) + 1); + } + + const items: StorageActivityBar[] = []; + for (let offset = 6; offset >= 0; offset -= 1) { + const date = new Date(today); + date.setDate(today.getDate() - offset); + const key = date.toISOString().slice(0, 10); + const label = `${String(date.getDate()).padStart(2, '0')}.${String(date.getMonth() + 1).padStart(2, '0')}`; + items.push({ label, fullLabel: key, value: counts.get(key) || 0, height: 0 }); + } + + const maxValue = Math.max(...items.map((item) => item.value), 0); + return items.map((item) => ({ + ...item, + height: item.value > 0 && maxValue > 0 ? Math.max(16, Math.round((item.value / maxValue) * 100)) : 0 + })); + } + + get routerBackupRows(): RouterBackupBar[] { + if (!this.backups.length) { + return []; + } + + const counters = new Map(); + for (const backup of this.backups) { + const entry = counters.get(backup.router_id) || { label: backup.router_name || `#${backup.router_id}`, value: 0, percent: 0 }; + entry.value += 1; + counters.set(backup.router_id, entry); + } + + const sorted = Array.from(counters.values()).sort((a, b) => b.value - a.value).slice(0, 5); + const maxValue = Math.max(...sorted.map((item) => item.value), 0); + return sorted.map((item) => ({ ...item, percent: maxValue > 0 ? (item.value / maxValue) * 100 : 0 })); + } + + get averageBackupsPerRouter(): string { + if (!this.data?.routers_count) { + return '0'; + } + return (this.data.total_backups / this.data.routers_count).toFixed(1); + } + + get coveragePercent(): number { + if (!this.routers.length) { + return 0; + } + const routersWithBackups = new Set(this.backups.map((item) => item.router_id)).size; + return Math.round((routersWithBackups / this.routers.length) * 100); + } + + get exportsSharePercent(): number { + if (!this.backups.length) { + return 0; + } + return Math.round((this.backups.filter((item) => item.backup_type === 'export').length / this.backups.length) * 100); + } + + get backupsLast7Days(): number { + const threshold = Date.now() - 7 * 24 * 60 * 60 * 1000; + return this.backups.filter((item) => new Date(item.created_at).getTime() >= threshold).length; + } + + get latestBackupLabel(): string { + const latest = this.latestBackup; + if (!latest) { + return this.ui.instant('dashboard.noneLabel'); + } + return latest.router_name || `#${latest.router_id}`; + } + + get latestBackupHint(): string { + const latest = this.latestBackup; + if (!latest) { + return this.ui.instant('dashboard.noActivity'); + } + return `${latest.backup_type === 'export' ? this.ui.instant('files.exportType') : this.ui.instant('files.binaryType')} · ${this.relativeAge(latest.created_at)}`; + } + + get busiestRouterLabel(): string { + const busiest = this.busiestRouter; + if (!busiest) { + return this.ui.instant('dashboard.noneLabel'); + } + return busiest.name; + } + + get busiestRouterHint(): string { + const busiest = this.busiestRouter; + if (!busiest) { + return this.ui.instant('dashboard.noActivity'); + } + return this.ui.instant('dashboard.routerSnapshotsHint', { count: busiest.count }); + } + + get activityToday(): number { + if (!this.data?.recent_logs?.length) { + return 0; + } + const today = new Date(); + return this.data.recent_logs.filter((log) => { + const value = new Date(log.timestamp); + return value.getFullYear() === today.getFullYear() && value.getMonth() === today.getMonth() && value.getDate() === today.getDate(); + }).length; + } + + get activityPageCount(): number { + const total = this.data?.recent_logs?.length || 0; + return Math.max(1, Math.ceil(total / this.activityPageSize)); + } + + get pagedRecentLogs() { + if (!this.data?.recent_logs?.length) { + return []; + } + const start = this.activityPage * this.activityPageSize; + return this.data.recent_logs.slice(start, start + this.activityPageSize); + } + + get activityRangeLabel(): string { + const total = this.data?.recent_logs?.length || 0; + if (!total) { + return '0 / 0'; + } + const start = this.activityPage * this.activityPageSize + 1; + const end = Math.min(total, start + this.activityPageSize - 1); + return `${start}-${end} / ${total}`; + } + + previousActivityPage() { + this.activityPage = Math.max(0, this.activityPage - 1); + } + + nextActivityPage() { + this.activityPage = Math.min(this.activityPageCount - 1, this.activityPage + 1); + } + + get storageRingBackground(): string { + const safePercent = Math.min(100, Math.max(0, this.usedPercent)); + return `conic-gradient(var(--accent) 0deg ${safePercent * 3.6}deg, rgba(129, 149, 167, 0.18) ${safePercent * 3.6}deg 360deg)`; + } + + formatBytes(value: number): string { + if (!value) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let size = value; + let unit = 0; + while (size >= 1024 && unit < units.length - 1) { + size /= 1024; + unit += 1; + } + return `${size.toFixed(size >= 10 || unit === 0 ? 0 : 1)} ${units[unit]}`; + } + + formatPercent(value: number): string { + return `${Number(value || 0).toFixed(1)}%`; + } + + relativeAge(value: string): string { + const diff = Date.now() - new Date(value).getTime(); + const hours = Math.floor(diff / 3_600_000); + if (hours < 1) { + const minutes = Math.max(1, Math.floor(diff / 60_000)); + return this.ui.instant('files.minutesAgo', { value: minutes }); + } + if (hours < 24) { + return this.ui.instant('files.hoursAgo', { value: hours }); + } + const days = Math.floor(hours / 24); + return this.ui.instant('files.daysAgo', { value: days }); + } + + activityTone(message: string): 'success' | 'danger' | 'warning' | 'info' { + const normalized = message.toLowerCase(); + if (normalized.includes('fail') || normalized.includes('error')) return 'danger'; + if (normalized.includes('cleanup') || normalized.includes('retention')) return 'warning'; + if (normalized.includes('upload') || normalized.includes('email')) return 'info'; + return 'success'; + } + + activityIcon(message: string): string { + const tone = this.activityTone(message); + if (tone === 'danger') return 'pi pi-exclamation-triangle'; + if (tone === 'warning') return 'pi pi-broom'; + if (tone === 'info') return 'pi pi-send'; + return 'pi pi-check'; + } + + activityLabel(message: string): string { + const tone = this.activityTone(message); + if (tone === 'danger') return this.ui.instant('dashboard.activityFailure'); + if (tone === 'warning') return this.ui.instant('dashboard.activityMaintenance'); + if (tone === 'info') return this.ui.instant('dashboard.activityDelivery'); + return this.ui.instant('dashboard.activitySuccess'); + } + + private get latestBackup(): BackupInventoryItem | undefined { + return this.backups.slice().sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0]; + } + + private get busiestRouter(): { name: string; count: number } | null { + if (!this.backups.length) { + return null; + } + const counters = new Map(); + for (const backup of this.backups) { + const entry = counters.get(backup.router_id) || { name: backup.router_name || `#${backup.router_id}`, count: 0 }; + entry.count += 1; + counters.set(backup.router_id, entry); + } + return Array.from(counters.values()).sort((a, b) => b.count - a.count)[0] || null; + } +} diff --git a/frontend/src/app/features/diff-configs/diff-configs-page.component.html b/frontend/src/app/features/diff-configs/diff-configs-page.component.html new file mode 100644 index 0000000..b3303c0 --- /dev/null +++ b/frontend/src/app/features/diff-configs/diff-configs-page.component.html @@ -0,0 +1,143 @@ + + +
+ + + + +
+ + +
+
+ + + + +
+ + + +
+
+ +
+
+
+ {{ 'files.compareOlder' | translate }} + +
+ +
+ {{ item.file_name }} + {{ item.router_name || item.router_id }} · {{ relativeAge(item.created_at) }} +
+ +
+
+
+ + + +
+
+ {{ 'files.compareNewer' | translate }} + +
+ +
+ {{ item.file_name }} + {{ item.router_name || item.router_id }} · {{ relativeAge(item.created_at) }} +
+ +
+
+
+
+
+
+ + + + + + {{ 'files.fileColumn' | translate }} + {{ 'files.routerColumn' | translate }} + {{ 'files.createdColumn' | translate }} + {{ 'files.compareColumn' | translate }} + + + + + +
{{ item.file_name }}
+ {{ 'files.checksum' | translate }}: {{ checksumShort(item.checksum) }} + + +
{{ item.router_name || item.router_id }}
+ ID {{ item.router_id }} + + +
{{ item.created_at | date: 'dd.MM.yyyy HH:mm' }}
+ {{ relativeAge(item.created_at) }} + + +
+ + + +
+ + +
+
+
+ + +
{{ viewedExport }}
+
+ + +
+
+
+
{{ diff.left_file_name }}
+ {{ 'files.compareOlder' | translate }} +
+
+
+
{{ diff.right_file_name }}
+ {{ 'files.compareNewer' | translate }} +
+
+ +{{ diff.stats.added }} + -{{ diff.stats.removed }} + ~{{ diff.stats.modified }} +
+
+ +
+
+ +
+
+
+ {{ line.left_number || '' }} +
{{ line.left_text || ' ' }}
+
+
+ {{ line.right_number || '' }} +
{{ line.right_text || ' ' }}
+
+
+
+ + +
{{ diff.diff_text }}
+
+
+
diff --git a/frontend/src/app/features/diff-configs/diff-configs-page.component.ts b/frontend/src/app/features/diff-configs/diff-configs-page.component.ts new file mode 100644 index 0000000..e000d7d --- /dev/null +++ b/frontend/src/app/features/diff-configs/diff-configs-page.component.ts @@ -0,0 +1,248 @@ +import { CommonModule } from '@angular/common'; +import { HttpParams } from '@angular/common/http'; +import { Component, OnInit, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { TranslateModule } from '@ngx-translate/core'; +import { ButtonModule } from 'primeng/button'; +import { DialogModule } from 'primeng/dialog'; +import { DropdownModule } from 'primeng/dropdown'; +import { TableModule } from 'primeng/table'; +import { TagModule } from 'primeng/tag'; + +import { ApiService } from '../../core/services/api.service'; +import { UiService } from '../../core/services/ui.service'; +import { PageHeaderComponent } from '../../shared/ui/page-header.component'; +import { SectionCardComponent } from '../../shared/ui/section-card.component'; +import { StatCardComponent } from '../../shared/ui/stat-card.component'; + +interface BackupFile { + id: number; + router_id: number; + router_name?: string; + file_name: string; + backup_type: 'export' | 'binary'; + created_at: string; + checksum?: string | null; + file_size?: number | null; +} + +interface BackupDiffLine { + type: 'context' | 'added' | 'removed' | 'modified'; + left_number?: number | null; + right_number?: number | null; + left_text: string; + right_text: string; +} + +interface BackupDiffStats { + added: number; + removed: number; + modified: number; + context: number; +} + +interface BackupDiffResponse { + left_backup_id: number; + right_backup_id: number; + left_file_name?: string | null; + right_file_name?: string | null; + diff_text: string; + lines: BackupDiffLine[]; + stats?: BackupDiffStats | null; +} + +@Component({ + standalone: true, + imports: [CommonModule, FormsModule, TranslateModule, ButtonModule, DialogModule, DropdownModule, TableModule, TagModule, PageHeaderComponent, SectionCardComponent, StatCardComponent], + templateUrl: './diff-configs-page.component.html' +}) +export class DiffConfigsPageComponent implements OnInit { + private readonly api = inject(ApiService); + private readonly ui = inject(UiService); + + files: BackupFile[] = []; + routers: { id: number; name: string }[] = []; + routerId: number | null = null; + compareLeftId: number | null = null; + compareRightId: number | null = null; + previewVisible = false; + diffVisible = false; + compareBusy = false; + loading = false; + previewTitle = ''; + viewedExport = ''; + diffData: BackupDiffResponse | null = null; + lastCompared: { left: number; right: number } | null = null; + + get routerOptions() { + return [{ label: this.ui.instant('files.allRouters'), value: null }, ...this.routers.map((item) => ({ label: item.name, value: item.id }))]; + } + + get exportFiles(): BackupFile[] { + return this.files + .filter((item) => item.backup_type === 'export' && (this.routerId === null || item.router_id === this.routerId)) + .slice() + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); + } + + get compareOptions() { + return this.exportFiles.map((item) => ({ + label: `${item.router_name || item.router_id} · ${item.file_name}`, + value: item.id + })); + } + + get compareLeft(): BackupFile | undefined { + return this.files.find((item) => item.id === this.compareLeftId); + } + + get compareRight(): BackupFile | undefined { + return this.files.find((item) => item.id === this.compareRightId); + } + + get compareReady(): boolean { + return !!this.compareLeftId && !!this.compareRightId && this.compareLeftId !== this.compareRightId; + } + + get availableExportsCount(): number { + return this.exportFiles.length; + } + + get selectedRouterLabel(): string { + if (this.routerId === null) { + return this.ui.instant('files.allRouters'); + } + return this.routers.find((item) => item.id === this.routerId)?.name || `#${this.routerId}`; + } + + get lastDiffLabel(): string { + if (!this.diffData?.left_file_name || !this.diffData?.right_file_name) { + return this.ui.instant('diffConfigs.noneSelected'); + } + return `${this.diffData.left_file_name} → ${this.diffData.right_file_name}`; + } + + ngOnInit() { + this.api.http.get(`${this.api.baseUrl}/routers`).subscribe((routers) => { + this.routers = routers.map((item) => ({ id: item.id, name: item.name })); + }); + this.load(); + } + + load() { + this.loading = true; + let params = new HttpParams().set('backup_type', 'export').set('sort_by', 'created_at').set('order', 'desc'); + if (this.routerId !== null) { + params = params.set('router_id', String(this.routerId)); + } + this.api.http.get(`${this.api.baseUrl}/backups`, { params }).subscribe({ + next: (files) => { + this.files = files; + if (this.compareLeftId && !this.files.some((item) => item.id === this.compareLeftId)) { + this.compareLeftId = null; + } + if (this.compareRightId && !this.files.some((item) => item.id === this.compareRightId)) { + this.compareRightId = null; + } + }, + complete: () => { + this.loading = false; + } + }); + } + + assignCompare(side: 'left' | 'right', item: BackupFile) { + if (side === 'left') { + this.compareLeftId = item.id; + return; + } + this.compareRightId = item.id; + } + + fillLatestPair() { + if (this.exportFiles.length < 2) { + return; + } + const [right, left] = this.exportFiles; + this.compareLeftId = left.id; + this.compareRightId = right.id; + } + + swapCompare() { + const left = this.compareLeftId; + this.compareLeftId = this.compareRightId; + this.compareRightId = left; + } + + viewExport(item: BackupFile) { + this.api.http.get<{ content: string }>(`${this.api.baseUrl}/backups/${item.id}/view`).subscribe((response) => { + this.viewedExport = response.content; + this.previewTitle = item.file_name; + this.ui.clear(); + this.previewVisible = true; + }); + } + + openStructuredDiff() { + if (!this.compareReady || this.compareBusy || !this.compareLeftId || !this.compareRightId) { + return; + } + this.compareBusy = true; + const [left, right] = this.sortPairByDate(this.compareLeftId, this.compareRightId); + this.lastCompared = { left, right }; + this.api.http.get(`${this.api.baseUrl}/backups/${left}/diff/${right}`).subscribe({ + next: (response) => { + this.diffData = response; + this.ui.clear(); + this.diffVisible = true; + }, + complete: () => { + this.compareBusy = false; + } + }); + } + + openHtmlDiff() { + if (!this.lastCompared) { + return; + } + this.api.http + .get(`${this.api.baseUrl}/backups/${this.lastCompared.left}/diff/${this.lastCompared.right}/html`, { responseType: 'text' }) + .subscribe((html) => { + const blob = new Blob([html], { type: 'text/html;charset=utf-8' }); + const url = URL.createObjectURL(blob); + window.open(url, '_blank'); + setTimeout(() => URL.revokeObjectURL(url), 60_000); + }); + } + + relativeAge(value: string): string { + const diff = Date.now() - new Date(value).getTime(); + const hours = Math.floor(diff / 3_600_000); + if (hours < 1) { + const minutes = Math.max(1, Math.floor(diff / 60_000)); + return this.ui.instant('files.minutesAgo', { value: minutes }); + } + if (hours < 24) { + return this.ui.instant('files.hoursAgo', { value: hours }); + } + const days = Math.floor(hours / 24); + return this.ui.instant('files.daysAgo', { value: days }); + } + + checksumShort(value?: string | null): string { + if (!value) { + return 'n/a'; + } + return `${value.slice(0, 8)}…${value.slice(-6)}`; + } + + private sortPairByDate(firstId: number, secondId: number): [number, number] { + const first = this.files.find((item) => item.id === firstId); + const second = this.files.find((item) => item.id === secondId); + if (!first || !second) { + return [firstId, secondId]; + } + return new Date(first.created_at).getTime() <= new Date(second.created_at).getTime() ? [firstId, secondId] : [secondId, firstId]; + } +} diff --git a/frontend/src/app/features/files/files-page.component.html b/frontend/src/app/features/files/files-page.component.html new file mode 100644 index 0000000..93bf98f --- /dev/null +++ b/frontend/src/app/features/files/files-page.component.html @@ -0,0 +1,186 @@ + +
+ + +
+
+ +
+ + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ +
+
+
+ {{ 'files.compareTitle' | translate }} +

{{ 'files.compareSubtitle' | translate }}

+
+
+ + +
+
+ +
+
+ + +
+ + + +
+ + +
+ +
+ + +
+
+
+
+ + + + + + + {{ 'files.fileColumn' | translate }} + {{ 'files.routerColumn' | translate }} + {{ 'files.typeColumn' | translate }} + {{ 'files.createdColumn' | translate }} + {{ 'files.sizeColumn' | translate }} + {{ 'files.compareColumn' | translate }} + {{ 'files.actionsColumn' | translate }} + + + + + + +
{{ item.file_name }}
+ {{ 'files.checksum' | translate }}: {{ checksumShort(item.checksum) }} + + +
{{ item.router_name || item.router_id }}
+ ID {{ item.router_id }} + + + +
{{ item.created_at | date: 'dd.MM.yyyy HH:mm' }}
+ {{ relativeAge(item.created_at) }} + + +
{{ formatBytes(item.file_size) }}
+ {{ item.backup_type === 'export' ? '.rsc' : '.backup' }} + + +
+ + + +
+ + {{ 'files.binaryNoCompare' | translate }} + + + +
+ + + + + +
+ + +
+
+
+ + +
{{ viewedExport }}
+
+ + +
+
+
+
{{ diff.left_file_name }}
+ {{ 'files.compareOlder' | translate }} +
+
+
+
{{ diff.right_file_name }}
+ {{ 'files.compareNewer' | translate }} +
+
+ +{{ diff.stats.added }} + -{{ diff.stats.removed }} + ~{{ diff.stats.modified }} +
+
+ + +
+
+ +
+
+
+ {{ line.left_number || '' }} +
{{ line.left_text || ' ' }}
+
+
+ {{ line.right_number || '' }} +
{{ line.right_text || ' ' }}
+
+
+
+ + +
{{ diff.diff_text }}
+
+
+
diff --git a/frontend/src/app/features/files/files-page.component.ts b/frontend/src/app/features/files/files-page.component.ts new file mode 100644 index 0000000..3cc51a9 --- /dev/null +++ b/frontend/src/app/features/files/files-page.component.ts @@ -0,0 +1,439 @@ +import { CommonModule } from '@angular/common'; +import { HttpParams, HttpResponse } from '@angular/common/http'; +import { Component, OnInit, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { TranslateModule } from '@ngx-translate/core'; +import { ButtonModule } from 'primeng/button'; +import { DialogModule } from 'primeng/dialog'; +import { DropdownModule } from 'primeng/dropdown'; +import { InputTextModule } from 'primeng/inputtext'; +import { TableModule } from 'primeng/table'; +import { TagModule } from 'primeng/tag'; + +import { ApiService } from '../../core/services/api.service'; +import { UiService } from '../../core/services/ui.service'; +import { PageHeaderComponent } from '../../shared/ui/page-header.component'; +import { SectionCardComponent } from '../../shared/ui/section-card.component'; +import { StatCardComponent } from '../../shared/ui/stat-card.component'; + +interface BackupFile { + id: number; + router_id: number; + router_name?: string; + file_name: string; + backup_type: 'export' | 'binary'; + created_at: string; + checksum?: string | null; + file_size?: number | null; +} + +interface BackupDiffLine { + type: 'context' | 'added' | 'removed' | 'modified'; + left_number?: number | null; + right_number?: number | null; + left_text: string; + right_text: string; +} + +interface BackupDiffStats { + added: number; + removed: number; + modified: number; + context: number; +} + +interface BackupDiffResponse { + left_backup_id: number; + right_backup_id: number; + left_file_name?: string | null; + right_file_name?: string | null; + diff_text: string; + lines: BackupDiffLine[]; + stats?: BackupDiffStats | null; +} + +@Component({ + standalone: true, + imports: [CommonModule, FormsModule, TranslateModule, ButtonModule, DialogModule, DropdownModule, InputTextModule, TableModule, TagModule, PageHeaderComponent, SectionCardComponent, StatCardComponent], + templateUrl: './files-page.component.html' +}) +export class FilesPageComponent implements OnInit { + private readonly api = inject(ApiService); + private readonly ui = inject(UiService); + + files: BackupFile[] = []; + selected: BackupFile[] = []; + routers: { id: number; name: string }[] = []; + search = ''; + backupType: 'export' | 'binary' | '' = ''; + routerId: number | null = null; + sortBy = 'created_at'; + order: 'asc' | 'desc' = 'desc'; + diffText = ''; + viewedExport = ''; + previewTitle = ''; + loading = false; + bulkBusy = false; + compareBusy = false; + previewVisible = false; + diffVisible = false; + compareLeftId: number | null = null; + compareRightId: number | null = null; + diffData: BackupDiffResponse | null = null; + lastCompared: { left: number; right: number } | null = null; + + get typeOptions() { + return [ + { label: this.ui.instant('files.allTypes'), value: '' }, + { label: this.ui.instant('files.exportType'), value: 'export' }, + { label: this.ui.instant('files.binaryType'), value: 'binary' } + ]; + } + + get sortOptions() { + return [ + { label: this.ui.instant('files.sortNewest'), value: 'created_at' }, + { label: this.ui.instant('files.sortName'), value: 'file_name' }, + { label: this.ui.instant('files.sortRouter'), value: 'router_name' }, + { label: this.ui.instant('files.sortType'), value: 'backup_type' } + ]; + } + + get orderOptions() { + return [ + { label: this.ui.instant('common.desc'), value: 'desc' }, + { label: this.ui.instant('common.asc'), value: 'asc' } + ]; + } + + get routerOptions() { + return [{ label: this.ui.instant('files.allRouters'), value: null }, ...this.routers.map((r) => ({ label: r.name, value: r.id }))]; + } + + get compareOptions() { + return this.exportFiles.map((item) => ({ + label: `${item.router_name || item.router_id} · ${item.file_name}`, + value: item.id + })); + } + + get exportFiles(): BackupFile[] { + return this.files + .filter((item) => item.backup_type === 'export') + .slice() + .sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()); + } + + get selectedIds(): number[] { + return this.selected.map((item) => item.id); + } + + get exportCount(): number { + return this.files.filter((item) => item.backup_type === 'export').length; + } + + get binaryCount(): number { + return this.files.filter((item) => item.backup_type === 'binary').length; + } + + get compareLeft(): BackupFile | undefined { + return this.files.find((item) => item.id === this.compareLeftId); + } + + get compareRight(): BackupFile | undefined { + return this.files.find((item) => item.id === this.compareRightId); + } + + get compareReady(): boolean { + return !!this.compareLeftId && !!this.compareRightId && this.compareLeftId !== this.compareRightId; + } + + get compareContextLabel(): string { + if (!this.compareReady) { + return this.ui.instant('files.compareSelectionHint'); + } + const left = this.compareLeft; + const right = this.compareRight; + const routerName = left?.router_name || right?.router_name || ''; + if (left && right && left.router_id === right.router_id) { + return this.ui.instant('files.compareReadySameRouter', { router: routerName || left.router_id }); + } + return this.ui.instant('files.compareReadyMixedRouters'); + } + + compareRole(item: BackupFile): 'left' | 'right' | '' { + if (item.id === this.compareLeftId) { + return 'left'; + } + if (item.id === this.compareRightId) { + return 'right'; + } + return ''; + } + + ngOnInit() { + this.api.http.get(`${this.api.baseUrl}/routers`).subscribe((routers) => { + this.routers = routers.map((item) => ({ id: item.id, name: item.name })); + }); + this.load(); + } + + load() { + this.loading = true; + let params = new HttpParams().set('sort_by', this.sortBy).set('order', this.order); + if (this.search.trim()) params = params.set('search', this.search.trim()); + if (this.backupType) params = params.set('backup_type', this.backupType); + if (this.routerId !== null) params = params.set('router_id', String(this.routerId)); + + this.api.http.get(`${this.api.baseUrl}/backups`, { params }).subscribe({ + next: (files) => { + this.files = files; + this.selected = []; + if (this.compareLeftId && !this.files.some((item) => item.id === this.compareLeftId)) { + this.compareLeftId = null; + } + if (this.compareRightId && !this.files.some((item) => item.id === this.compareRightId)) { + this.compareRightId = null; + } + }, + complete: () => { + this.loading = false; + } + }); + } + + resetFilters() { + this.search = ''; + this.backupType = ''; + this.routerId = null; + this.sortBy = 'created_at'; + this.order = 'desc'; + this.load(); + } + + download(id: number) { + this.api.http + .get(`${this.api.baseUrl}/backups/${id}/download`, { observe: 'response', responseType: 'blob' }) + .subscribe((response) => this.openBlob(response, `backup-${id}`)); + } + + viewExport(item: BackupFile) { + this.api.http.get<{ content: string }>(`${this.api.baseUrl}/backups/${item.id}/view`).subscribe((response) => { + this.viewedExport = response.content; + this.previewTitle = item.file_name; + this.ui.clear(); + this.previewVisible = true; + }); + } + + sendEmail(id: number) { + this.api.http.post(`${this.api.baseUrl}/backups/${id}/email`, {}).subscribe(() => { + this.ui.success('toast.backupSentEmail'); + }); + } + + upload(item: BackupFile) { + this.api.http.post(`${this.api.baseUrl}/backups/router/${item.router_id}/upload/${item.id}`, {}).subscribe(() => { + this.ui.success('toast.binaryUploaded'); + }); + } + + async deleteOne(id: number) { + const accepted = await this.ui.confirm({ + messageKey: 'confirm.deleteBackup', + acceptKey: 'common.delete' + }); + if (!accepted) { + return; + } + this.api.http.delete(`${this.api.baseUrl}/backups/${id}`).subscribe(() => { + this.ui.success('toast.backupDeleted'); + this.load(); + }); + } + + assignCompare(side: 'left' | 'right', item: BackupFile) { + if (item.backup_type !== 'export') { + return; + } + if (side === 'left') { + this.compareLeftId = item.id; + } else { + this.compareRightId = item.id; + } + } + + fillLatestPair() { + if (this.exportFiles.length < 2) { + return; + } + const latest = this.exportFiles[this.exportFiles.length - 1]; + const previous = this.exportFiles[this.exportFiles.length - 2]; + this.compareLeftId = previous.id; + this.compareRightId = latest.id; + } + + compareClosestForRouter(item: BackupFile) { + const candidates = this.exportFiles.filter((entry) => entry.router_id === item.router_id); + if (candidates.length < 2) { + return; + } + const targetIndex = candidates.findIndex((entry) => entry.id === item.id); + const older = candidates[Math.max(0, targetIndex - 1)]; + const newer = candidates[Math.min(candidates.length - 1, targetIndex + 1)]; + const left = older && older.id !== item.id ? older : item; + const right = newer && newer.id !== item.id ? newer : item; + if (left.id === right.id) { + return; + } + this.setComparePair(left.id, right.id); + this.openStructuredDiff(); + } + + swapCompare() { + const left = this.compareLeftId; + this.compareLeftId = this.compareRightId; + this.compareRightId = left; + } + + openStructuredDiff() { + if (!this.compareReady || this.compareBusy || !this.compareLeftId || !this.compareRightId) { + return; + } + this.compareBusy = true; + const [left, right] = this.sortPairByDate(this.compareLeftId, this.compareRightId); + this.lastCompared = { left, right }; + this.api.http.get(`${this.api.baseUrl}/backups/${left}/diff/${right}`).subscribe({ + next: (response) => { + this.diffData = response; + this.diffText = response.diff_text; + this.ui.clear(); + this.diffVisible = true; + }, + complete: () => { + this.compareBusy = false; + } + }); + } + + openHtmlDiff() { + if (!this.lastCompared) { + return; + } + this.api.http + .get(`${this.api.baseUrl}/backups/${this.lastCompared.left}/diff/${this.lastCompared.right}/html`, { responseType: 'text' }) + .subscribe((html) => { + const blob = new Blob([html], { type: 'text/html;charset=utf-8' }); + const url = URL.createObjectURL(blob); + window.open(url, '_blank'); + setTimeout(() => URL.revokeObjectURL(url), 60_000); + }); + } + + async bulkDelete() { + if (!this.selectedIds.length || this.bulkBusy) { + return; + } + const accepted = await this.ui.confirm({ + messageKey: 'confirm.deleteSelectedFiles', + params: { count: this.selectedIds.length }, + acceptKey: 'common.delete' + }); + if (!accepted) { + return; + } + this.bulkBusy = true; + this.api.http.post(`${this.api.baseUrl}/backups/bulk`, { action: 'delete', backup_ids: this.selectedIds }).subscribe({ + next: () => { + this.ui.success('toast.selectedBackupsDeleted'); + this.load(); + }, + complete: () => { + this.bulkBusy = false; + } + }); + } + + bulkDownload() { + if (!this.selectedIds.length || this.bulkBusy) { + return; + } + this.bulkBusy = true; + this.api.http + .post(`${this.api.baseUrl}/backups/bulk`, { action: 'download', backup_ids: this.selectedIds }, { responseType: 'blob' }) + .subscribe({ + next: (blob: Blob) => { + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = 'routeros-backups.zip'; + link.click(); + setTimeout(() => URL.revokeObjectURL(url), 60_000); + this.ui.success('toast.archivePrepared'); + }, + complete: () => { + this.bulkBusy = false; + } + }); + } + + formatBytes(value?: number | null): string { + if (!value) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let size = value; + let unit = 0; + while (size >= 1024 && unit < units.length - 1) { + size /= 1024; + unit += 1; + } + return `${size.toFixed(size >= 10 || unit === 0 ? 0 : 1)} ${units[unit]}`; + } + + relativeAge(value: string): string { + const diff = Date.now() - new Date(value).getTime(); + const hours = Math.floor(diff / 3_600_000); + if (hours < 1) { + const minutes = Math.max(1, Math.floor(diff / 60_000)); + return this.ui.instant('files.minutesAgo', { value: minutes }); + } + if (hours < 24) { + return this.ui.instant('files.hoursAgo', { value: hours }); + } + const days = Math.floor(hours / 24); + return this.ui.instant('files.daysAgo', { value: days }); + } + + checksumShort(value?: string | null): string { + if (!value) { + return 'n/a'; + } + return `${value.slice(0, 8)}…${value.slice(-6)}`; + } + + private setComparePair(firstId: number, secondId: number) { + const [left, right] = this.sortPairByDate(firstId, secondId); + this.compareLeftId = left; + this.compareRightId = right; + } + + private sortPairByDate(firstId: number, secondId: number): [number, number] { + const first = this.files.find((item) => item.id === firstId); + const second = this.files.find((item) => item.id === secondId); + if (!first || !second) { + return [firstId, secondId]; + } + return new Date(first.created_at).getTime() <= new Date(second.created_at).getTime() ? [firstId, secondId] : [secondId, firstId]; + } + + private openBlob(response: HttpResponse, fallbackName: string) { + const disposition = response.headers.get('content-disposition') || ''; + const match = disposition.match(/filename="?([^";]+)"?/i); + const filename = match?.[1] || fallbackName; + const url = URL.createObjectURL(response.body || new Blob()); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + setTimeout(() => URL.revokeObjectURL(url), 60_000); + } +} diff --git a/frontend/src/app/features/logs/logs-page.component.html b/frontend/src/app/features/logs/logs-page.component.html new file mode 100644 index 0000000..22024d8 --- /dev/null +++ b/frontend/src/app/features/logs/logs-page.component.html @@ -0,0 +1,25 @@ + +
+ + +
+
+ +
+
+ {{ retentionDays }} {{ 'logs.daysSuffix' | translate }} + {{ 'logs.retentionInfoLabel' | translate }} +
+
+ + + + {{ 'logs.timestampColumn' | translate }}{{ 'logs.messageColumn' | translate }} + + + {{ log.timestamp }} +
{{ log.message }}
+ +
+
+
diff --git a/frontend/src/app/features/logs/logs-page.component.ts b/frontend/src/app/features/logs/logs-page.component.ts new file mode 100644 index 0000000..4cb3cd8 --- /dev/null +++ b/frontend/src/app/features/logs/logs-page.component.ts @@ -0,0 +1,63 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnInit, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { TranslateModule } from '@ngx-translate/core'; +import { ButtonModule } from 'primeng/button'; +import { InputTextModule } from 'primeng/inputtext'; +import { TableModule } from 'primeng/table'; + +import { ApiService } from '../../core/services/api.service'; +import { UiService } from '../../core/services/ui.service'; +import { PageHeaderComponent } from '../../shared/ui/page-header.component'; +import { SectionCardComponent } from '../../shared/ui/section-card.component'; + +@Component({ + standalone: true, + imports: [CommonModule, FormsModule, TranslateModule, ButtonModule, InputTextModule, TableModule, PageHeaderComponent, SectionCardComponent], + templateUrl: './logs-page.component.html' +}) +export class LogsPageComponent implements OnInit { + private readonly api = inject(ApiService); + private readonly ui = inject(UiService); + + logs: any[] = []; + days = 7; + retentionDays = 7; + cleaning = false; + + ngOnInit() { + this.load(); + this.api.http.get(`${this.api.baseUrl}/settings`).subscribe((settings) => { + this.retentionDays = Number(settings?.log_retention_days || 7); + this.days = this.retentionDays; + }); + } + + load() { + this.api.http.get(`${this.api.baseUrl}/logs`).subscribe((r) => (this.logs = r)); + } + + async cleanup() { + if (this.cleaning) { + return; + } + const accepted = await this.ui.confirm({ + messageKey: 'confirm.deleteLogsOlderThan', + params: { days: this.days }, + acceptKey: 'common.delete' + }); + if (!accepted) { + return; + } + this.cleaning = true; + this.api.http.delete(`${this.api.baseUrl}/logs/older-than/${this.days}`).subscribe({ + next: () => { + this.ui.success('toast.logsDeletedOlderThan', { days: this.days }); + this.load(); + }, + complete: () => { + this.cleaning = false; + } + }); + } +} diff --git a/frontend/src/app/features/routers/router-detail-page.component.html b/frontend/src/app/features/routers/router-detail-page.component.html new file mode 100644 index 0000000..9805278 --- /dev/null +++ b/frontend/src/app/features/routers/router-detail-page.component.html @@ -0,0 +1,164 @@ + +
+ + + + +
+
+ +
+ + + + +
+ +
+ +
+
+
{{ 'routers.connectionStateTitle' | translate }}{{ connection.success ? ('common.ok' | translate) : ('common.failed' | translate) }}
+
{{ 'routers.lastTestAt' | translate }}{{ connection.tested_at | date:'short' }}
+
{{ 'routers.hostname' | translate }}{{ connection.hostname }}
+
{{ 'routers.model' | translate }}{{ connection.model }}
+
{{ 'routers.version' | translate }}{{ connection.version || 'n/a' }}
+
{{ 'routers.uptime' | translate }}{{ connection.uptime }}
+
+
+ {{ 'routers.lastError' | translate }} + {{ connection.error }} +
+
+ +
+ +

{{ 'routers.noConnection' | translate }}

+
+
+
+ +
+ +
+
+ {{ previewTitle }} + {{ 'routers.previewModalHint' | translate }} +
+
+ +
+
+ +
+ +

{{ 'routers.noPreview' | translate }}

+
+
+
+ + +
+
+ {{ diffData.left_file_name }} → {{ diffData.right_file_name }} + {{ 'routers.diffModalHint' | translate }} +
+
+ +
+
+ +
+ +

{{ 'routers.noDiff' | translate }}

+
+
+
+
+
+ +
+ + + + {{ 'files.fileColumn' | translate }}{{ 'files.createdColumn' | translate }}{{ 'common.actions' | translate }} + + + + +
{{ item.file_name }}
+ {{ 'files.exportType' | translate }} + + {{ item.created_at }} + +
+ + + + + +
+ + +
+
+
+ + + + + {{ 'files.fileColumn' | translate }}{{ 'files.createdColumn' | translate }}{{ 'common.actions' | translate }} + + + + +
{{ item.file_name }}
+ {{ 'files.binaryType' | translate }} + + {{ item.created_at }} + +
+ + + + +
+ + +
+
+
+
+ + +
{{ exportContent }}
+
+ + +
+
+
+
{{ diff.left_file_name }}
+ {{ 'files.compareOlder' | translate }} +
+
+
+
{{ diff.right_file_name }}
+ {{ 'files.compareNewer' | translate }} +
+
+ +{{ diff.stats.added }} + -{{ diff.stats.removed }} + ~{{ diff.stats.modified }} +
+
+
{{ diff.diff_text }}
+
+ +
{{ diffText }}
+
+
diff --git a/frontend/src/app/features/routers/router-detail-page.component.ts b/frontend/src/app/features/routers/router-detail-page.component.ts new file mode 100644 index 0000000..1377fba --- /dev/null +++ b/frontend/src/app/features/routers/router-detail-page.component.ts @@ -0,0 +1,286 @@ +import { CommonModule } from '@angular/common'; +import { HttpResponse } from '@angular/common/http'; +import { Component, OnInit, inject } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { ButtonModule } from 'primeng/button'; +import { DialogModule } from 'primeng/dialog'; +import { TableModule } from 'primeng/table'; +import { TagModule } from 'primeng/tag'; + +import { ApiService } from '../../core/services/api.service'; +import { UiService } from '../../core/services/ui.service'; +import { PageHeaderComponent } from '../../shared/ui/page-header.component'; +import { SectionCardComponent } from '../../shared/ui/section-card.component'; +import { StatCardComponent } from '../../shared/ui/stat-card.component'; + +interface BackupItem { + id: number; + file_name: string; + backup_type: 'export' | 'binary'; + created_at: string; +} + +interface ConnectionSnapshot { + success: boolean; + tested_at: string; + hostname: string; + model: string; + version?: string | null; + uptime: string; + error?: string | null; +} + +interface BackupDiffStats { + added: number; + removed: number; + modified: number; + context: number; +} + +interface BackupDiffResponse { + left_backup_id: number; + right_backup_id: number; + left_file_name?: string | null; + right_file_name?: string | null; + diff_text: string; + stats?: BackupDiffStats | null; +} + +@Component({ + standalone: true, + imports: [CommonModule, TranslateModule, ButtonModule, DialogModule, TableModule, TagModule, PageHeaderComponent, SectionCardComponent, StatCardComponent], + templateUrl: './router-detail-page.component.html' +}) +export class RouterDetailPageComponent implements OnInit { + private readonly route = inject(ActivatedRoute); + private readonly api = inject(ApiService); + private readonly router = inject(Router); + private readonly ui = inject(UiService); + + routerId!: number; + routerItem: any; + backups: BackupItem[] = []; + connection: ConnectionSnapshot | null = null; + exportContent = ''; + diffText = ''; + previewTitle = ''; + previewVisible = false; + diffVisible = false; + diffData: BackupDiffResponse | null = null; + exporting = false; + runningBinary = false; + testing = false; + deletingRouter = false; + + get exportBackups(): BackupItem[] { + return this.backups.filter((item) => item.backup_type === 'export'); + } + + get binaryBackups(): BackupItem[] { + return this.backups.filter((item) => item.backup_type === 'binary'); + } + + get connectionStateLabel(): string { + if (!this.connection) { + return this.ui.instant('common.idle'); + } + return this.connection.success ? this.ui.instant('common.ok') : this.ui.instant('common.failed'); + } + + get hasPreview(): boolean { + return !!this.exportContent; + } + + get hasDiff(): boolean { + return !!this.diffText; + } + + ngOnInit() { + this.routerId = Number(this.route.snapshot.paramMap.get('id')); + this.load(); + } + + load() { + this.api.http.get(`${this.api.baseUrl}/routers/${this.routerId}`).subscribe((routerItem: any) => { + this.routerItem = routerItem; + this.connection = this.mapStoredConnection(routerItem); + }); + this.api.http.get(`${this.api.baseUrl}/backups/router/${this.routerId}`).subscribe((r) => (this.backups = r)); + } + + runExport() { + if (this.exporting) { + return; + } + this.exporting = true; + this.api.http.post(`${this.api.baseUrl}/backups/router/${this.routerId}/export`, {}).subscribe({ + next: () => { + this.ui.success('toast.exportCreated'); + this.load(); + }, + complete: () => { + this.exporting = false; + } + }); + } + + runBinary() { + if (this.runningBinary) { + return; + } + this.runningBinary = true; + this.api.http.post(`${this.api.baseUrl}/backups/router/${this.routerId}/binary`, {}).subscribe({ + next: () => { + this.ui.success('toast.binaryCreated'); + this.load(); + }, + complete: () => { + this.runningBinary = false; + } + }); + } + + testConnection() { + if (this.testing) { + return; + } + this.testing = true; + this.api.http.get(`${this.api.baseUrl}/routers/${this.routerId}/test-connection`).subscribe({ + next: (result) => { + this.connection = result; + this.syncStoredConnection(result); + if (result.success) { + this.ui.success('toast.connectionSuccessful'); + } else { + this.ui.error('toast.connectionFailed'); + } + }, + complete: () => { + this.testing = false; + } + }); + } + + compareToLatest(id: number) { + const latest = this.exportBackups[0]; + if (!latest || latest.id === id) { + return; + } + this.api.http.get(`${this.api.baseUrl}/backups/${id}/diff/${latest.id}`).subscribe((response) => { + this.diffData = response; + this.diffText = response.diff_text; + this.ui.clear(); + this.diffVisible = true; + }); + } + + async remove(id: number) { + const accepted = await this.ui.confirm({ messageKey: 'confirm.deleteBackup', acceptKey: 'common.delete' }); + if (!accepted) { + return; + } + this.api.http.delete(`${this.api.baseUrl}/backups/${id}`).subscribe(() => { + this.ui.success('toast.backupDeleted'); + this.load(); + }); + } + + upload(id: number) { + this.api.http.post(`${this.api.baseUrl}/backups/router/${this.routerId}/upload/${id}`, {}).subscribe(() => { + this.ui.success('toast.binaryUploaded'); + }); + } + + async deleteRouter() { + if (this.deletingRouter) { + return; + } + const accepted = await this.ui.confirm({ messageKey: 'confirm.deleteRouterWithFiles', acceptKey: 'common.delete' }); + if (!accepted) { + return; + } + this.deletingRouter = true; + this.api.http.delete(`${this.api.baseUrl}/routers/${this.routerId}`).subscribe({ + next: () => this.router.navigate(['/routers']), + complete: () => { + this.deletingRouter = false; + } + }); + } + + download(id: number) { + this.api.http + .get(`${this.api.baseUrl}/backups/${id}/download`, { observe: 'response', responseType: 'blob' }) + .subscribe((response) => this.openBlob(response, `backup-${id}`)); + } + + viewExport(id: number) { + const backup = this.exportBackups.find((item) => item.id === id); + this.api.http.get(`${this.api.baseUrl}/backups/${id}/view`).subscribe((r) => { + this.exportContent = r.content; + this.previewTitle = backup?.file_name || this.ui.instant('routers.previewTitle'); + this.ui.clear(); + this.previewVisible = true; + }); + } + + sendEmail(id: number) { + this.api.http.post(`${this.api.baseUrl}/backups/${id}/email`, {}).subscribe(() => { + this.ui.success('toast.backupSentEmail'); + }); + } + + openPreviewModal() { + this.ui.clear(); + this.previewVisible = true; + } + + openDiffModal() { + this.ui.clear(); + this.diffVisible = true; + } + + private mapStoredConnection(routerItem: any): ConnectionSnapshot | null { + if (!routerItem?.last_connection_tested_at) { + return null; + } + return { + success: Boolean(routerItem.last_connection_status), + tested_at: routerItem.last_connection_tested_at, + hostname: routerItem.last_connection_hostname || routerItem.name, + model: routerItem.last_connection_model || 'Unknown', + version: routerItem.last_connection_version, + uptime: routerItem.last_connection_uptime || 'Unknown', + error: routerItem.last_connection_error || null + }; + } + + private syncStoredConnection(result: ConnectionSnapshot) { + if (!this.routerItem) { + return; + } + this.routerItem = { + ...this.routerItem, + last_connection_status: result.success, + last_connection_tested_at: result.tested_at, + last_connection_hostname: result.hostname, + last_connection_model: result.model, + last_connection_version: result.version, + last_connection_uptime: result.uptime, + last_connection_error: result.error, + }; + } + + private openBlob(response: HttpResponse, fallbackName: string) { + const disposition = response.headers.get('content-disposition') || ''; + const match = disposition.match(/filename="?([^";]+)"?/i); + const filename = match?.[1] || fallbackName; + const url = URL.createObjectURL(response.body || new Blob()); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + setTimeout(() => URL.revokeObjectURL(url), 60_000); + } +} diff --git a/frontend/src/app/features/routers/routers-page.component.html b/frontend/src/app/features/routers/routers-page.component.html new file mode 100644 index 0000000..9cab121 --- /dev/null +++ b/frontend/src/app/features/routers/routers-page.component.html @@ -0,0 +1,88 @@ + +
+ +
+
+ +
+
+ {{ routers.length }} + {{ 'routers.registeredDevices' | translate }} +
+
+
+ {{ keyCount }} + {{ 'routers.summaryKeyAccess' | translate }} +
+
+
+ {{ passwordCount }} + {{ 'routers.summaryPasswordAccess' | translate }} +
+
+ + + + + {{ 'routers.name' | translate }}{{ 'routers.endpoint' | translate }}{{ 'routers.access' | translate }}{{ 'common.actions' | translate }} + + + + +
{{ routerItem.name }}
+ {{ 'routers.routerOsTarget' | translate }} + + +
{{ routerItem.host }}:{{ routerItem.port }}
+ {{ routerItem.ssh_user }} + + +
+ + +
+ + +
+ + + +
+ + +
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
diff --git a/frontend/src/app/features/routers/routers-page.component.ts b/frontend/src/app/features/routers/routers-page.component.ts new file mode 100644 index 0000000..47a1273 --- /dev/null +++ b/frontend/src/app/features/routers/routers-page.component.ts @@ -0,0 +1,131 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnInit, inject } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { ButtonModule } from 'primeng/button'; +import { DialogModule } from 'primeng/dialog'; +import { InputTextareaModule } from 'primeng/inputtextarea'; +import { InputTextModule } from 'primeng/inputtext'; +import { TableModule } from 'primeng/table'; +import { TagModule } from 'primeng/tag'; + +import { ApiService } from '../../core/services/api.service'; +import { UiService } from '../../core/services/ui.service'; +import { PageHeaderComponent } from '../../shared/ui/page-header.component'; +import { SectionCardComponent } from '../../shared/ui/section-card.component'; + +interface RouterItem { + id: number; + name: string; + host: string; + port: number; + ssh_user: string; + ssh_password?: string; + ssh_key?: string; +} + +@Component({ + standalone: true, + imports: [CommonModule, ReactiveFormsModule, TranslateModule, ButtonModule, DialogModule, InputTextModule, InputTextareaModule, TableModule, TagModule, PageHeaderComponent, SectionCardComponent], + templateUrl: './routers-page.component.html' +}) +export class RoutersPageComponent implements OnInit { + private readonly api = inject(ApiService); + private readonly fb = inject(FormBuilder); + private readonly router = inject(Router); + private readonly ui = inject(UiService); + + visible = false; + editingId: number | null = null; + saving = false; + routers: RouterItem[] = []; + readonly form = this.fb.nonNullable.group({ + name: ['', Validators.required], + host: ['', Validators.required], + port: [22, Validators.required], + ssh_user: ['admin', Validators.required], + ssh_password: '', + ssh_key: '' + }); + + get dialogTitle(): string { + return this.ui.instant(this.editingId ? 'routers.editDialogTitle' : 'routers.createDialogTitle'); + } + + get passwordCount(): number { + return this.routers.filter((item) => !!item.ssh_password).length; + } + + get keyCount(): number { + return this.routers.filter((item) => !!item.ssh_key).length; + } + + + ngOnInit() { + this.load(); + } + + load() { + this.api.http.get(`${this.api.baseUrl}/routers`).subscribe((r) => (this.routers = r)); + } + + openCreate() { + this.editingId = null; + this.form.reset({ name: '', host: '', port: 22, ssh_user: 'admin', ssh_password: '', ssh_key: '' }); + this.visible = true; + } + + edit(item: RouterItem) { + this.editingId = item.id; + this.form.reset({ + name: item.name, + host: item.host, + port: item.port, + ssh_user: item.ssh_user, + ssh_password: item.ssh_password ?? '', + ssh_key: item.ssh_key ?? '' + }); + this.visible = true; + } + + save() { + if (this.form.invalid || this.saving) { + return; + } + this.saving = true; + const request$ = this.editingId + ? this.api.http.put(`${this.api.baseUrl}/routers/${this.editingId}`, this.form.getRawValue()) + : this.api.http.post(`${this.api.baseUrl}/routers`, this.form.getRawValue()); + + request$.subscribe({ + next: () => { + this.ui.success(this.editingId ? 'toast.routerUpdated' : 'toast.routerCreated'); + this.visible = false; + this.load(); + }, + complete: () => { + this.saving = false; + } + }); + } + + async remove(id: number) { + const accepted = await this.ui.confirm({ + messageKey: 'confirm.deleteRouterWithFiles', + acceptKey: 'common.delete' + }); + if (!accepted) { + return; + } + this.api.http.delete(`${this.api.baseUrl}/routers/${id}`).subscribe(() => { + this.ui.success('toast.routerDeleted'); + this.load(); + }); + } + + + open(id: number) { + this.router.navigate(['/routers', id]); + } +} diff --git a/frontend/src/app/features/settings/settings-page.component.html b/frontend/src/app/features/settings/settings-page.component.html new file mode 100644 index 0000000..283f6c1 --- /dev/null +++ b/frontend/src/app/features/settings/settings-page.component.html @@ -0,0 +1,308 @@ + +
+ + +
+
+ +
+
+
+ {{ job.label | translate }} + +
+
+
{{ job.description | translate: job.description_params }}
+ {{ job.valid ? ((job.next_runs[0] | date:'short') || ('settings.noNextRun' | translate)) : job.error }} +
+
+
+ +
+
+
+
+ + {{ 'settings.automationTitle' | translate }} + {{ 'settings.automationSubtitle' | translate }} + +
+
+
+ {{ 'settings.automationPlannerTitle' | translate }} +

{{ 'settings.automationPlannerSubtitle' | translate }}

+
+ +
+ +
+
+
+
+ {{ 'settings.exportScheduleTitle' | translate }} + {{ scheduleSummary(scheduleEditors.export) }} +
+ +
+
{{ 'settings.exportPlannerHint' | translate }}
+
+ + + + + + +
+ + : + +
+
+ + + + + + + + +
+
+ +
+
+
+ {{ 'settings.binaryScheduleTitle' | translate }} + {{ scheduleSummary(scheduleEditors.binary) }} +
+ +
+
{{ 'settings.binaryPlannerHint' | translate }}
+
+ + + + + + +
+ + : + +
+
+ + + + + + + + +
+
+ +
+
+
+ {{ 'settings.retentionTitle' | translate }} + {{ scheduleSummary(scheduleEditors.retention) }} +
+ +
+
{{ 'settings.retentionPlannerHint' | translate }}
+
+ + + + + + + + + + + + + + +
+ + : + +
+
+ + + + + + + + +
+
+ +
+
+
+ {{ 'settings.connectionTestsTitle' | translate }} + {{ connectionTestSummary() }} +
+ +
+
{{ 'settings.connectionTestsHint' | translate }}
+
+ + + + +
+
+
+
+
+ +
+ + {{ 'settings.interfaceTitle' | translate }} + {{ 'settings.interfaceSubtitle' | translate }} + +
+
+
+ {{ 'settings.interfacePreferencesTitle' | translate }} +

{{ 'settings.interfacePreferencesHint' | translate }}

+
+ +
+ +
+ + + + + + + + +
+
+
+ +
+ + {{ 'settings.notificationsTitle' | translate }} + {{ 'settings.notificationsSubtitle' | translate }} + +
+
+
+
+ {{ 'settings.smtpEnabled' | translate }} + {{ 'settings.smtpEnabledHint' | translate }} +
+
+ + +
+
+
+
+ {{ 'settings.failuresOnly' | translate }} + {{ 'settings.failuresOnlyHint' | translate }} +
+
+ + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+
+ + {{ 'settings.sshDefaultsTitle' | translate }} + {{ 'settings.sshDefaultsSubtitle' | translate }} + +
+
+
+
+ {{ 'settings.globalSshPrivateKey' | translate }} +

{{ 'settings.sshKeyHelper' | translate }}

+
+ + +
+ +
+

{{ 'settings.sshRevealHint' | translate }}

+
+ + +
+
+ +
+
+ +
+ + +
+ +
+ + +
+ + {{ 'settings.sshKeyClearNotice' | translate }} +
+
+
+
+
+ +
+ +
+
diff --git a/frontend/src/app/features/settings/settings-page.component.ts b/frontend/src/app/features/settings/settings-page.component.ts new file mode 100644 index 0000000..3a02351 --- /dev/null +++ b/frontend/src/app/features/settings/settings-page.component.ts @@ -0,0 +1,490 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnDestroy, OnInit, effect, inject } from '@angular/core'; +import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { TranslateModule } from '@ngx-translate/core'; +import { ButtonModule } from 'primeng/button'; +import { DropdownModule } from 'primeng/dropdown'; +import { InputTextModule } from 'primeng/inputtext'; +import { InputTextareaModule } from 'primeng/inputtextarea'; +import { TagModule } from 'primeng/tag'; +import { Subject, finalize, forkJoin, takeUntil } from 'rxjs'; + +import { ApiService } from '../../core/services/api.service'; +import { AuthService } from '../../core/services/auth.service'; +import { AppFont, FontService } from '../../core/services/font.service'; +import { APP_LANGUAGE_OPTIONS, AppLanguage, LanguageService } from '../../core/services/language.service'; +import { UiService } from '../../core/services/ui.service'; +import { PageHeaderComponent } from '../../shared/ui/page-header.component'; + +interface SchedulerJobStatus { + key: string; + label: string; + enabled: boolean; + cron?: string | null; + description: string; + description_params?: Record | null; + valid: boolean; + next_runs: string[]; + error?: string | null; +} + +interface SchedulerStatusResponse { + timezone: string; + running: boolean; + jobs: SchedulerJobStatus[]; +} + +type ScheduleMode = 'disabled' | 'daily' | 'weekly' | 'custom'; +type BooleanSettingControl = 'smtp_notifications_enabled' | 'notify_failures_only'; + +interface ScheduleEditor { + mode: ScheduleMode; + hour: string; + minute: string; + weekday: string; + cron: string; +} + +interface SettingsResponse { + backup_retention_days: number; + log_retention_days: number; + export_cron: string; + binary_cron: string; + retention_cron: string; + enable_auto_export: boolean; + connection_test_interval_minutes: number; + global_ssh_key: string | null; + has_global_ssh_key: boolean; + pushover_token: string | null; + pushover_userkey: string | null; + notify_failures_only: boolean; + smtp_host: string | null; + smtp_port: number; + smtp_login: string | null; + smtp_password: string | null; + smtp_notifications_enabled: boolean; + recipient_email: string | null; +} + +@Component({ + standalone: true, + imports: [CommonModule, FormsModule, ReactiveFormsModule, TranslateModule, ButtonModule, DropdownModule, InputTextModule, InputTextareaModule, TagModule, PageHeaderComponent], + templateUrl: './settings-page.component.html' +}) +export class SettingsPageComponent implements OnInit, OnDestroy { + private readonly fb = inject(FormBuilder); + private readonly api = inject(ApiService); + private readonly auth = inject(AuthService); + private readonly ui = inject(UiService); + private readonly language = inject(LanguageService); + private readonly font = inject(FontService); + private readonly destroy$ = new Subject(); + + saving = false; + testingEmail = false; + testingPushover = false; + unlockingSshKey = false; + schedulerStatus?: SchedulerStatusResponse; + hasStoredSshKey = false; + sshKeyVisible = false; + clearStoredSshKey = false; + sshRevealPassword = ''; + + readonly scheduleEditors: Record<'export' | 'binary' | 'retention', ScheduleEditor> = { + export: this.createEditor(), + binary: this.createEditor(), + retention: this.createEditor() + }; + readonly form = this.fb.nonNullable.group({ + backup_retention_days: [7, Validators.required], + log_retention_days: [7, Validators.required], + export_cron: '', + binary_cron: '', + retention_cron: '', + enable_auto_export: false, + connection_test_interval_minutes: [0, Validators.min(0)], + global_ssh_key: '', + pushover_token: '', + pushover_userkey: '', + notify_failures_only: true, + smtp_host: '', + smtp_port: 587, + smtp_login: '', + smtp_password: '', + smtp_notifications_enabled: false, + recipient_email: '', + preferred_language: 'pl' as AppLanguage, + preferred_font: 'default' as AppFont + }); + + constructor() { + effect(() => { + const user = this.auth.user(); + if (!user) { + return; + } + this.form.patchValue( + { + preferred_language: user.preferred_language || 'pl', + preferred_font: user.preferred_font || 'default' + }, + { emitEvent: false } + ); + }); + } + + get scheduleModeOptions() { + return [ + { label: this.ui.instant('settings.scheduleDisabled'), value: 'disabled' }, + { label: this.ui.instant('settings.scheduleDaily'), value: 'daily' }, + { label: this.ui.instant('settings.scheduleWeekly'), value: 'weekly' }, + { label: this.ui.instant('settings.scheduleCustom'), value: 'custom' } + ]; + } + + get weekdayOptions() { + return [ + { label: this.ui.instant('settings.weekdayMonday'), value: '1' }, + { label: this.ui.instant('settings.weekdayTuesday'), value: '2' }, + { label: this.ui.instant('settings.weekdayWednesday'), value: '3' }, + { label: this.ui.instant('settings.weekdayThursday'), value: '4' }, + { label: this.ui.instant('settings.weekdayFriday'), value: '5' }, + { label: this.ui.instant('settings.weekdaySaturday'), value: '6' }, + { label: this.ui.instant('settings.weekdaySunday'), value: '0' } + ]; + } + + readonly languageOptions = APP_LANGUAGE_OPTIONS.map((option) => ({ + label: `${option.flag} ${option.label}`, + value: option.code + })); + + get fontOptions() { + return [ + { label: this.ui.instant('settings.fontDefault'), value: 'default' }, + { label: 'Adwaita Mono', value: 'adwaita_mono' }, + { label: 'Hack', value: 'hack' } + ]; + } + + ngOnInit() { + this.form.controls.preferred_language.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((selectedLanguage) => { + this.previewLanguage(selectedLanguage as AppLanguage | null); + }); + this.reloadSettings(); + this.loadSchedulerStatus(); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + save() { + if (this.form.invalid || this.saving) { + return; + } + this.syncEditorsToForm(); + this.saving = true; + + const preferredLanguage = this.form.controls.preferred_language.value as AppLanguage; + const preferredFont = this.form.controls.preferred_font.value as AppFont; + + forkJoin({ + settings: this.api.http.put(`${this.api.baseUrl}/settings`, this.buildPayload()), + preferences: this.auth.updatePreferences({ preferred_language: preferredLanguage, preferred_font: preferredFont }) + }) + .pipe( + finalize(() => { + this.saving = false; + }) + ) + .subscribe({ + next: () => { + this.font.set(preferredFont); + this.ui.success('toast.settingsSaved'); + this.reloadSettings(); + this.loadSchedulerStatus(); + }, + error: () => { + this.ui.error('toast.settingsSaveFailed'); + } + }); + } + + testEmail() { + if (this.testingEmail) { + return; + } + this.testingEmail = true; + this.api.http + .post(`${this.api.baseUrl}/settings/test-email`, {}) + .pipe( + finalize(() => { + this.testingEmail = false; + }) + ) + .subscribe({ + next: () => this.ui.success('toast.testEmailSent'), + error: () => { + this.ui.error('toast.testEmailFailed'); + } + }); + } + + testPushover() { + if (this.testingPushover) { + return; + } + this.testingPushover = true; + this.api.http + .post(`${this.api.baseUrl}/settings/test-pushover`, {}) + .pipe( + finalize(() => { + this.testingPushover = false; + }) + ) + .subscribe({ + next: () => this.ui.success('toast.testPushoverSent'), + error: () => { + this.ui.error('toast.testPushoverFailed'); + } + }); + } + + unlockSshKey() { + if (this.unlockingSshKey) { + return; + } + if (!this.sshRevealPassword.trim()) { + this.ui.error('settings.sshRevealPasswordRequired'); + return; + } + this.unlockingSshKey = true; + this.api.http + .post<{ global_ssh_key: string | null }>(`${this.api.baseUrl}/settings/reveal-ssh-key`, { password: this.sshRevealPassword }) + .pipe( + finalize(() => { + this.unlockingSshKey = false; + }) + ) + .subscribe({ + next: (response) => { + this.form.controls.global_ssh_key.setValue(response.global_ssh_key || ''); + this.sshKeyVisible = true; + this.clearStoredSshKey = false; + this.ui.success('toast.sshKeyUnlocked'); + }, + error: () => { + this.ui.error('settings.sshRevealPasswordInvalid'); + } + }); + } + + hideSshKey() { + if (!this.hasStoredSshKey) { + return; + } + this.sshKeyVisible = false; + this.form.controls.global_ssh_key.setValue(''); + this.sshRevealPassword = ''; + } + + clearSshKey() { + this.clearStoredSshKey = true; + this.hasStoredSshKey = false; + this.sshKeyVisible = true; + this.form.controls.global_ssh_key.setValue(''); + } + + schedulerJob(key: string): SchedulerJobStatus | undefined { + return this.schedulerStatus?.jobs.find((item) => item.key === key); + } + + scheduleSummary(editor: ScheduleEditor): string { + if (editor.mode === 'disabled') { + return this.ui.instant('settings.scheduleDisabledHint'); + } + if (editor.mode === 'daily') { + return this.ui.instant('settings.scheduleDailySummary', { time: `${editor.hour}:${editor.minute}` }); + } + if (editor.mode === 'weekly') { + const weekday = this.weekdayOptions.find((item) => item.value === editor.weekday)?.label || ''; + return this.ui.instant('settings.scheduleWeeklySummary', { weekday, time: `${editor.hour}:${editor.minute}` }); + } + return editor.cron || this.ui.instant('settings.scheduleCustomEmpty'); + } + + connectionTestSummary(): string { + const minutes = Number(this.form.controls.connection_test_interval_minutes.value || 0); + return minutes > 0 ? this.ui.instant('settings.connectionTestsEverySummary', { minutes }) : this.ui.instant('settings.connectionTestsDisabledHint'); + } + + scheduleEnabled(editor: ScheduleEditor): boolean { + return editor.mode !== 'disabled'; + } + + scheduleSeverity(editor: ScheduleEditor): 'success' | 'secondary' { + return this.scheduleEnabled(editor) ? 'success' : 'secondary'; + } + + connectionTestSeverity(): 'success' | 'secondary' { + return Number(this.form.controls.connection_test_interval_minutes.value || 0) > 0 ? 'success' : 'secondary'; + } + + setBooleanSetting(controlName: BooleanSettingControl, value: boolean) { + this.form.controls[controlName].setValue(value); + this.form.controls[controlName].markAsDirty(); + } + + normalizeTime(editor: ScheduleEditor) { + editor.hour = this.padTime(editor.hour, 23); + editor.minute = this.padTime(editor.minute, 59); + } + + previewFont() { + this.font.set(this.form.controls.preferred_font.value as AppFont); + } + + previewLanguage(selectedLanguage?: AppLanguage | null) { + const nextLanguage = (selectedLanguage || this.form.controls.preferred_language.value || 'pl') as AppLanguage; + this.form.controls.preferred_language.setValue(nextLanguage, { emitEvent: false }); + if (this.auth.user()) { + this.language.setForAuthenticatedUser(nextLanguage); + return; + } + this.language.set(nextLanguage); + } + + private createEditor(): ScheduleEditor { + return { mode: 'disabled', hour: '02', minute: '00', weekday: '1', cron: '' }; + } + + private reloadSettings() { + this.api.http.get(`${this.api.baseUrl}/settings`).subscribe((response) => { + this.hasStoredSshKey = response.has_global_ssh_key; + this.sshKeyVisible = !response.has_global_ssh_key; + this.clearStoredSshKey = false; + this.sshRevealPassword = ''; + const user = this.auth.user(); + this.form.patchValue({ + backup_retention_days: response.backup_retention_days, + log_retention_days: response.log_retention_days, + export_cron: response.export_cron || '', + binary_cron: response.binary_cron || '', + retention_cron: response.retention_cron || '', + enable_auto_export: response.enable_auto_export, + connection_test_interval_minutes: Number(response.connection_test_interval_minutes || 0), + global_ssh_key: '', + pushover_token: response.pushover_token || '', + pushover_userkey: response.pushover_userkey || '', + notify_failures_only: response.notify_failures_only, + smtp_host: response.smtp_host || '', + smtp_port: Number(response.smtp_port || 587), + smtp_login: response.smtp_login || '', + smtp_password: response.smtp_password || '', + smtp_notifications_enabled: response.smtp_notifications_enabled, + recipient_email: response.recipient_email || '', + preferred_language: (user?.preferred_language || this.language.current() || 'pl') as AppLanguage, + preferred_font: (user?.preferred_font || 'default') as AppFont + }, { emitEvent: false }); + this.hydrateEditors(); + }); + } + + private buildPayload() { + const raw = this.form.getRawValue(); + const normalizedKey = raw.global_ssh_key.trim(); + return { + backup_retention_days: Number(raw.backup_retention_days || 0), + log_retention_days: Number(raw.log_retention_days || 0), + export_cron: (raw.export_cron || '').trim(), + binary_cron: (raw.binary_cron || '').trim(), + retention_cron: (raw.retention_cron || '').trim(), + enable_auto_export: Boolean(raw.enable_auto_export), + connection_test_interval_minutes: Number(raw.connection_test_interval_minutes || 0), + global_ssh_key: normalizedKey || null, + pushover_token: this.normalizeOptionalText(raw.pushover_token), + pushover_userkey: this.normalizeOptionalText(raw.pushover_userkey), + notify_failures_only: Boolean(raw.notify_failures_only), + smtp_host: this.normalizeOptionalText(raw.smtp_host), + smtp_port: Number(raw.smtp_port || 587), + smtp_login: this.normalizeOptionalText(raw.smtp_login), + smtp_password: this.normalizeOptionalText(raw.smtp_password), + smtp_notifications_enabled: Boolean(raw.smtp_notifications_enabled), + recipient_email: this.normalizeOptionalText(raw.recipient_email), + clear_global_ssh_key: this.clearStoredSshKey + }; + } + + private hydrateEditors() { + const exportEditor = this.editorFromCron(this.form.controls.export_cron.value); + if (!this.form.controls.enable_auto_export.value) { + exportEditor.mode = 'disabled'; + } + this.scheduleEditors.export = exportEditor; + this.scheduleEditors.binary = this.editorFromCron(this.form.controls.binary_cron.value); + this.scheduleEditors.retention = this.editorFromCron(this.form.controls.retention_cron.value); + } + + private syncEditorsToForm() { + this.normalizeTime(this.scheduleEditors.export); + this.normalizeTime(this.scheduleEditors.binary); + this.normalizeTime(this.scheduleEditors.retention); + this.form.patchValue({ + export_cron: this.editorToCron(this.scheduleEditors.export), + binary_cron: this.editorToCron(this.scheduleEditors.binary), + retention_cron: this.editorToCron(this.scheduleEditors.retention), + enable_auto_export: this.scheduleEditors.export.mode !== 'disabled' + }); + } + + private editorFromCron(cron: string): ScheduleEditor { + const normalized = (cron || '').trim(); + if (!normalized) { + return this.createEditor(); + } + + const parts = normalized.split(/\s+/); + if (parts.length === 5) { + const [minute, hour, day, month, dayOfWeek] = parts; + if (day === '*' && month === '*' && dayOfWeek === '*') { + return { mode: 'daily', hour: this.padTime(hour, 23), minute: this.padTime(minute, 59), weekday: '1', cron: normalized }; + } + if (day === '*' && month === '*' && /^[0-7]$/.test(dayOfWeek)) { + return { mode: 'weekly', hour: this.padTime(hour, 23), minute: this.padTime(minute, 59), weekday: dayOfWeek, cron: normalized }; + } + } + + return { mode: 'custom', hour: '02', minute: '00', weekday: '1', cron: normalized }; + } + + private editorToCron(editor: ScheduleEditor): string { + if (editor.mode === 'disabled') { + return ''; + } + if (editor.mode === 'daily') { + return `${editor.minute} ${editor.hour} * * *`; + } + if (editor.mode === 'weekly') { + return `${editor.minute} ${editor.hour} * * ${editor.weekday}`; + } + return editor.cron.trim(); + } + + private padTime(value: string, max: number): string { + const numeric = Math.min(max, Math.max(0, Number(value || 0))); + return String(Math.floor(numeric)).padStart(2, '0'); + } + + private normalizeOptionalText(value: string | null | undefined): string | null { + const normalized = (value || '').trim(); + return normalized || null; + } + + private loadSchedulerStatus() { + this.api.http.get(`${this.api.baseUrl}/settings/scheduler-status`).subscribe((status) => { + this.schedulerStatus = status; + }); + } +} diff --git a/frontend/src/app/features/swos-beta/swos-beta-page.component.html b/frontend/src/app/features/swos-beta/swos-beta-page.component.html new file mode 100644 index 0000000..44701b0 --- /dev/null +++ b/frontend/src/app/features/swos-beta/swos-beta-page.component.html @@ -0,0 +1,80 @@ + +
+ +
+
+ + +
+
+ {{ 'switchosBeta.warningHeadline' | translate }} +

{{ 'switchosBeta.warningBody' | translate }}

+
+ +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+ + +
{{ 'switchosBeta.resultEmpty' | translate }}
+ +
+
+ {{ 'switchosBeta.baseUrl' | translate }} + {{ probeResult.base_url }} +
+
+ {{ 'switchosBeta.httpStatus' | translate }} + {{ probeResult.status_code }} +
+
+ {{ 'switchosBeta.authMode' | translate }} + {{ probeResult.auth_mode }} +
+
+ {{ 'switchosBeta.pageTitle' | translate }} + {{ probeResult.page_title || '—' }} +
+
+ {{ 'switchosBeta.serverHeader' | translate }} + {{ probeResult.server || '—' }} +
+
+ {{ 'switchosBeta.backupEndpoint' | translate }} + {{ (probeResult.backup_endpoint_ok ? 'switchosBeta.available' : 'switchosBeta.unavailable') | translate }} +
+
{{ probeResult.note }}
+
+ +
{{ lastError }}
+
+
diff --git a/frontend/src/app/features/swos-beta/swos-beta-page.component.ts b/frontend/src/app/features/swos-beta/swos-beta-page.component.ts new file mode 100644 index 0000000..97b9436 --- /dev/null +++ b/frontend/src/app/features/swos-beta/swos-beta-page.component.ts @@ -0,0 +1,131 @@ +import { CommonModule } from '@angular/common'; +import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { Component, inject } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { TranslateModule } from '@ngx-translate/core'; +import { finalize } from 'rxjs'; +import { ButtonModule } from 'primeng/button'; +import { InputTextModule } from 'primeng/inputtext'; +import { TagModule } from 'primeng/tag'; + +import { ApiService } from '../../core/services/api.service'; +import { UiService } from '../../core/services/ui.service'; +import { PageHeaderComponent } from '../../shared/ui/page-header.component'; +import { SectionCardComponent } from '../../shared/ui/section-card.component'; + +interface SwosBetaProbeResult { + success: boolean; + base_url: string; + status_code: number; + auth_mode: string; + page_title?: string | null; + content_type?: string | null; + server?: string | null; + save_backup_visible: boolean; + backup_endpoint_ok: boolean; + note?: string | null; +} + +@Component({ + standalone: true, + imports: [CommonModule, ReactiveFormsModule, TranslateModule, ButtonModule, InputTextModule, TagModule, PageHeaderComponent, SectionCardComponent], + templateUrl: './swos-beta-page.component.html' +}) +export class SwosBetaPageComponent { + private readonly api = inject(ApiService); + private readonly fb = inject(FormBuilder); + private readonly ui = inject(UiService); + + probing = false; + downloading = false; + lastError = ''; + probeResult?: SwosBetaProbeResult; + + readonly form = this.fb.nonNullable.group({ + label: '', + host: ['', Validators.required], + port: [80, [Validators.required, Validators.min(1), Validators.max(65535)]], + username: ['admin', Validators.required], + password: '' + }); + + get formValue() { + return this.form.getRawValue(); + } + + probe() { + if (this.form.invalid || this.probing) { + this.form.markAllAsTouched(); + return; + } + + this.lastError = ''; + this.probing = true; + this.api.http + .post(`${this.api.baseUrl}/swos-beta/probe`, this.formValue) + .pipe(finalize(() => (this.probing = false))) + .subscribe({ + next: (result) => { + this.probeResult = result; + this.ui.success('toast.swosBetaProbeOk'); + }, + error: (error: HttpErrorResponse) => { + this.probeResult = undefined; + this.lastError = this.extractError(error); + this.ui.error('toast.swosBetaProbeFailed'); + } + }); + } + + download() { + if (this.form.invalid || this.downloading) { + this.form.markAllAsTouched(); + return; + } + + this.lastError = ''; + this.downloading = true; + this.api.http + .post(`${this.api.baseUrl}/swos-beta/download`, this.formValue, { + observe: 'response', + responseType: 'blob' + }) + .pipe(finalize(() => (this.downloading = false))) + .subscribe({ + next: (response) => { + this.saveBlob(response); + this.ui.success('toast.swosBetaDownloadOk'); + }, + error: (error: HttpErrorResponse) => { + this.lastError = this.extractError(error); + this.ui.error('toast.swosBetaDownloadFailed'); + } + }); + } + + private saveBlob(response: HttpResponse) { + const blob = response.body || new Blob(); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = this.extractFilename(response.headers.get('content-disposition')); + link.click(); + URL.revokeObjectURL(url); + } + + private extractFilename(disposition: string | null): string { + const match = disposition?.match(/filename="?([^\"]+)"?/i); + return match?.[1] || 'switchos-backup.swb'; + } + + private extractError(error: HttpErrorResponse): string { + const detail = error.error?.detail; + if (typeof detail === 'string' && detail.trim()) { + return detail.trim(); + } + if (typeof error.error === 'string' && error.error.trim()) { + return error.error.trim(); + } + return this.ui.instant('switchosBeta.genericError'); + } +} diff --git a/frontend/src/app/shared/auth/auth-toolbar.component.html b/frontend/src/app/shared/auth/auth-toolbar.component.html new file mode 100644 index 0000000..20f5487 --- /dev/null +++ b/frontend/src/app/shared/auth/auth-toolbar.component.html @@ -0,0 +1,15 @@ +
+ + + +
diff --git a/frontend/src/app/shared/auth/auth-toolbar.component.ts b/frontend/src/app/shared/auth/auth-toolbar.component.ts new file mode 100644 index 0000000..f9d8238 --- /dev/null +++ b/frontend/src/app/shared/auth/auth-toolbar.component.ts @@ -0,0 +1,31 @@ +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { ButtonModule } from 'primeng/button'; + +import { AppLanguage, LanguageService } from '../../core/services/language.service'; +import { ThemeService } from '../../core/services/theme.service'; + +@Component({ + selector: 'app-auth-toolbar', + standalone: true, + imports: [CommonModule, ButtonModule], + templateUrl: './auth-toolbar.component.html' +}) +export class AuthToolbarComponent { + readonly theme = inject(ThemeService); + readonly language = inject(LanguageService); + + get languageOptions() { + const current = this.language.current(); + return [ + { code: 'no', label: `${current === 'no' ? '✓ ' : ''}🇳🇴 Norsk` }, + { code: 'es', label: `${current === 'es' ? '✓ ' : ''}🇪🇸 Español` }, + { code: 'pl', label: `${current === 'pl' ? '✓ ' : ''}🇵🇱 Polski` }, + { code: 'en', label: `${current === 'en' ? '✓ ' : ''}🇬🇧 English` } + ]; + } + + changeLanguage(event: Event) { + this.language.set((event.target as HTMLSelectElement).value as AppLanguage); + } +} diff --git a/frontend/src/app/shared/layout/app-sidebar.component.html b/frontend/src/app/shared/layout/app-sidebar.component.html new file mode 100644 index 0000000..bb9f9ef --- /dev/null +++ b/frontend/src/app/shared/layout/app-sidebar.component.html @@ -0,0 +1,27 @@ + + + + + diff --git a/frontend/src/app/shared/layout/app-sidebar.component.ts b/frontend/src/app/shared/layout/app-sidebar.component.ts new file mode 100644 index 0000000..a460361 --- /dev/null +++ b/frontend/src/app/shared/layout/app-sidebar.component.ts @@ -0,0 +1,16 @@ +import { CommonModule } from '@angular/common'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { RouterLink, RouterLinkActive } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; + +@Component({ + selector: 'app-sidebar', + standalone: true, + imports: [CommonModule, RouterLink, RouterLinkActive, TranslateModule], + templateUrl: './app-sidebar.component.html' +}) +export class AppSidebarComponent { + @Input() collapsed = false; + @Input() items: Array<{ label: string; link: string; icon: string; exact?: boolean }> = []; + @Output() navigate = new EventEmitter(); +} diff --git a/frontend/src/app/shared/layout/app-topbar.component.html b/frontend/src/app/shared/layout/app-topbar.component.html new file mode 100644 index 0000000..847db42 --- /dev/null +++ b/frontend/src/app/shared/layout/app-topbar.component.html @@ -0,0 +1,35 @@ +
+
+ +
+
{{ 'topbar.caption' | translate }}
+
{{ pageTitle | translate }}
+
+
+ +
+ + + + +
+ +
+ {{ username }} + {{ 'topbar.role' | translate }} +
+
+ + +
+
diff --git a/frontend/src/app/shared/layout/app-topbar.component.ts b/frontend/src/app/shared/layout/app-topbar.component.ts new file mode 100644 index 0000000..2951703 --- /dev/null +++ b/frontend/src/app/shared/layout/app-topbar.component.ts @@ -0,0 +1,45 @@ +import { CommonModule } from '@angular/common'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { AvatarModule } from 'primeng/avatar'; +import { ButtonModule } from 'primeng/button'; + +export interface TopbarLanguageOption { + code: string; + label: string; + flag: string; +} + +@Component({ + selector: 'app-topbar', + standalone: true, + imports: [CommonModule, TranslateModule, ButtonModule, AvatarModule], + templateUrl: './app-topbar.component.html' +}) +export class AppTopbarComponent { + @Input() pageTitle = 'dashboard.title'; + @Input() username = 'admin'; + @Input() lang = 'pl'; + @Input() themeMode: 'light' | 'dark' = 'light'; + @Input() languages: TopbarLanguageOption[] = []; + @Output() menuClick = new EventEmitter(); + @Output() themeClick = new EventEmitter(); + @Output() languageChange = new EventEmitter(); + @Output() logoutClick = new EventEmitter(); + + get userInitials(): string { + return this.username.slice(0, 2).toUpperCase(); + } + + get displayLanguages(): TopbarLanguageOption[] { + return this.languages.map((option) => ({ + ...option, + label: `${option.flag} ${option.label}${option.code === this.lang ? ' ✓' : ''}` + })); + } + + onLanguageSelect(event: Event) { + const value = (event.target as HTMLSelectElement).value; + this.languageChange.emit(value); + } +} diff --git a/frontend/src/app/shared/ui/page-header.component.html b/frontend/src/app/shared/ui/page-header.component.html new file mode 100644 index 0000000..675eaa3 --- /dev/null +++ b/frontend/src/app/shared/ui/page-header.component.html @@ -0,0 +1,10 @@ + diff --git a/frontend/src/app/shared/ui/page-header.component.ts b/frontend/src/app/shared/ui/page-header.component.ts new file mode 100644 index 0000000..9336498 --- /dev/null +++ b/frontend/src/app/shared/ui/page-header.component.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-page-header', + standalone: true, + imports: [CommonModule], + templateUrl: './page-header.component.html' +}) +export class PageHeaderComponent { + @Input({ required: true }) title = ''; + @Input() subtitle = ''; + @Input() eyebrow = ''; +} diff --git a/frontend/src/app/shared/ui/section-card.component.html b/frontend/src/app/shared/ui/section-card.component.html new file mode 100644 index 0000000..96d8268 --- /dev/null +++ b/frontend/src/app/shared/ui/section-card.component.html @@ -0,0 +1,12 @@ + +
+
+

{{ title }}

+

{{ subtitle }}

+
+
+ +
+
+ +
diff --git a/frontend/src/app/shared/ui/section-card.component.ts b/frontend/src/app/shared/ui/section-card.component.ts new file mode 100644 index 0000000..642293c --- /dev/null +++ b/frontend/src/app/shared/ui/section-card.component.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { CardModule } from 'primeng/card'; + +@Component({ + selector: 'app-section-card', + standalone: true, + imports: [CommonModule, CardModule], + templateUrl: './section-card.component.html' +}) +export class SectionCardComponent { + @Input() title = ''; + @Input() subtitle = ''; +} diff --git a/frontend/src/app/shared/ui/stat-card.component.html b/frontend/src/app/shared/ui/stat-card.component.html new file mode 100644 index 0000000..23d6564 --- /dev/null +++ b/frontend/src/app/shared/ui/stat-card.component.html @@ -0,0 +1,13 @@ + +
+
+
{{ label }}
+
{{ value }}
+
{{ hint }}
+
+
+ +
+
+ +
diff --git a/frontend/src/app/shared/ui/stat-card.component.ts b/frontend/src/app/shared/ui/stat-card.component.ts new file mode 100644 index 0000000..c19ef09 --- /dev/null +++ b/frontend/src/app/shared/ui/stat-card.component.ts @@ -0,0 +1,20 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { CardModule } from 'primeng/card'; +import { TagModule } from 'primeng/tag'; + +@Component({ + selector: 'app-stat-card', + standalone: true, + imports: [CommonModule, CardModule, TagModule], + templateUrl: './stat-card.component.html' +}) +export class StatCardComponent { + @Input({ required: true }) label = ''; + @Input({ required: true }) value: string | number = ''; + @Input() hint = ''; + @Input() tag = ''; + @Input() icon = 'pi pi-chart-bar'; + @Input() iconClass = ''; + @Input() severity: 'success' | 'info' | 'warning' | 'danger' | 'secondary' | 'contrast' | undefined = 'info'; +} diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json new file mode 100644 index 0000000..74c4e48 --- /dev/null +++ b/frontend/src/assets/i18n/en.json @@ -0,0 +1,513 @@ +{ + "app": { + "menu": "Menu" + }, + "sidebar": { + "title": "MikroTik backup", + "subtitle": "RouterOS manager" + }, + "topbar": { + "caption": "mikrotik / control center", + "role": "administrator", + "languageSelector": "Language selector" + }, + "common": { + "apply": "Apply", + "reset": "Reset", + "delete": "Delete", + "confirm": "Confirm", + "cancel": "Cancel", + "download": "Download", + "email": "Send e-mail", + "preview": "Preview", + "restore": "Restore", + "actions": "Actions", + "open": "Open", + "edit": "Edit", + "diff": "Diff", + "ok": "OK", + "idle": "Idle", + "asc": "Ascending", + "desc": "Descending", + "enabled": "Enabled", + "disabled": "Disabled", + "failed": "Failed" + }, + "nav": { + "dashboard": "Dashboard", + "routers": "Routers", + "files": "Repository", + "settings": "Settings", + "logs": "Logs", + "logout": "Logout", + "theme": "Theme", + "changePassword": "Change password", + "diffConfigs": "Config diff", + "switchosBeta": "SwitchOS beta" + }, + "auth": { + "username": "Username", + "password": "Password", + "login": "Login", + "register": "Register", + "confirmPassword": "Confirm password", + "changePassword": "Change password", + "currentPassword": "Current password", + "newPassword": "New password", + "backToLogin": "Back to login", + "backToApp": "Back to app", + "loginSubtitle": "Sign in to continue.", + "loginFailed": "Login failed", + "accountCreated": "Account created", + "registrationFailed": "Registration failed", + "passwordsMismatch": "Passwords do not match", + "changePasswordFailed": "Password change failed", + "securityEyebrow": "account / security", + "changePasswordSubtitle": "Update the administrator password without extra visual clutter.", + "changePasswordCardSubtitle": "Enter the current password and set new credentials.", + "passwordPanelSubtitle": "Quick check for password strength and field consistency before saving.", + "passwordStrength": "Password strength", + "passwordWeak": "Weak", + "passwordMedium": "Medium", + "passwordStrong": "Strong", + "ruleLength": "At least 8 characters", + "ruleDigit": "At least one digit", + "ruleMatch": "Both fields match", + "passwordsMatchHint": "The new password and confirmation match." + }, + "dashboard": { + "title": "Dashboard", + "eyebrow": "home / dashboard", + "subtitle": "Overview of backups, exports and operational activity in one place.", + "exportAll": "Export all", + "binaryAll": "Binary backup", + "managedRouters": "Routers", + "managedRoutersHint": "All managed devices", + "inventoryTag": "Fleet", + "exportsCard": "Exports", + "exportsHint": "Readable configuration snapshots", + "textTag": "Text", + "binaryCard": "Binary backups", + "binaryHint": "Recovery points", + "binaryTag": "Binary", + "allFilesCard": "All files", + "allFilesHint": "Artifacts in the repository", + "archiveTag": "Archive", + "storageTitle": "Storage utilization", + "storageSubtitle": "Current overview of repository usage and free space.", + "folderUsage": "Folder usage", + "diskUsage": "Disk usage", + "totalDisk": "Total disk", + "freeSpace": "Free space", + "activityTitle": "Recent activity", + "activitySubtitle": "Latest operational events from the backend.", + "noActivity": "No recent events to display.", + "avgBackupsPerRouter": "Avg backups / router", + "activitySuccess": "Completed task", + "activityFailure": "Needs attention", + "activityMaintenance": "Maintenance", + "activityDelivery": "Delivery", + "operationsTitle": "Operations center", + "operationsSubtitle": "Primary actions and live repository indicators in one place.", + "latestSnapshot": "Latest snapshot", + "coverageLabel": "Fleet coverage", + "coverageHint": "Routers with at least one backup", + "weeklyActivityLabel": "7-day activity", + "weeklyActivityHint": "New backups created this week", + "busiestRouterLabel": "Busiest router", + "routerSnapshotsHint": "{{count}} snapshots in the repository", + "exportShareLabel": "Export share", + "activityTodayLabel": "Events today", + "noneLabel": "None", + "activityTodayHint": "Entries created today", + "usedSpace": "Used space", + "storageViewCapacity": "Capacity", + "storageViewCapacityHint": "Disk, repository usage and free space shown on one scale.", + "storageViewMix": "Backup types", + "storageViewMixHint": "Split of all snapshots into text exports and binary backups.", + "storageViewActivity": "7-day activity", + "storageViewActivityHint": "Number of new backups created during the last seven days.", + "storageViewRouters": "Top routers", + "storageViewRoutersHint": "Devices with the highest number of snapshots in the repository.", + "storageChartEmpty": "There is not enough data to draw this chart yet.", + "storageSnapshotTitle": "Repository metrics", + "storageSnapshotHint": "Quick snapshot of the most important storage and backup indicators." + }, + "routers": { + "title": "Routers", + "detailTitle": "Router details", + "add": "Add router", + "eyebrow": "device inventory", + "subtitle": "Manage RouterOS endpoints, credentials and fleet-wide backup jobs.", + "registeredDevices": "Registered devices", + "fleetTag": "Fleet", + "sshPassword": "SSH password", + "passwordHint": "Password-based access", + "credsTag": "Creds", + "sshKey": "SSH key", + "keyHint": "Key-based access", + "securityTag": "Security", + "defaultPort": "Port 22", + "defaultPortHint": "Standard SSH endpoints", + "portTag": "Port", + "listTitle": "Router list", + "listSubtitle": "Compact operational view of every managed device.", + "name": "Name", + "endpoint": "Endpoint", + "access": "Access", + "routerOsTarget": "RouterOS target", + "passwordMode": "Password", + "noPassword": "No password", + "keyMode": "Key", + "noKey": "No key", + "createDialogTitle": "Add router", + "editDialogTitle": "Edit router", + "host": "Host", + "port": "Port", + "sshUser": "SSH user", + "sshPrivateKey": "SSH private key", + "optionalPassword": "Optional password", + "optionalPrivateKey": "Optional private key", + "saveRouter": "Save router", + "profileEyebrow": "router profile", + "detailSubtitle": "Device operations and backup history", + "exportOne": "Export", + "binaryOne": "Binary", + "testConnection": "Test connection", + "deleteRouter": "Delete router", + "exportsLabel": "Exports", + "exportsLabelHint": "Text snapshots", + "binaryLabel": "Binary backups", + "binaryLabelHint": "Recovery images", + "connectionLabel": "Connection", + "connectionLabelHint": "Status from the latest automatic or manual connection test", + "probeTag": "Probe", + "accessTag": "Access", + "sshUserHint": "Current SSH user", + "deviceStatusTitle": "Device status", + "deviceStatusSubtitle": "Stored metadata from the latest automatic or manual connection test.", + "hostname": "Hostname", + "model": "Model", + "version": "Version", + "uptime": "Uptime", + "noConnection": "No saved connection test yet. Run a manual test or enable automatic checks in settings.", + "previewTitle": "Export preview", + "previewSubtitle": "Most recently opened export file.", + "noPreview": "Select an export file to preview its contents.", + "diffTitle": "Latest diff", + "diffSubtitle": "Difference against the newest export.", + "exportsTableTitle": "Exports", + "exportsTableSubtitle": "Readable RouterOS snapshots.", + "binaryTableTitle": "Binary backups", + "binaryTableSubtitle": "Files ready for device restore.", + "summaryKeyAccess": "with key-based access", + "summaryPasswordAccess": "with password access", + "connectionStateTitle": "Connection state", + "lastTestAt": "Last test", + "lastError": "Last error", + "deviceStatusManualHint": "Automatic checks use the interval from settings. The manual test button is still available.", + "previewModalHint": "The last opened export is available in a modal.", + "openPreviewModal": "Open preview", + "diffModalHint": "The last loaded diff is available in a modal.", + "openDiffModal": "Open diff", + "noDiff": "Choose an export and run a diff to see the latest comparison." + }, + "files": { + "title": "Repository", + "eyebrow": "artifact repository", + "subtitle": "Search, compare and deliver backups from one clear view.", + "downloadZip": "Download ZIP", + "visibleFiles": "Visible files", + "visibleFilesHint": "Current filter result", + "liveTag": "Live", + "selected": "Selected", + "selectedHint": "Ready for bulk actions", + "batchTag": "Batch", + "exportsCard": "Exports", + "exportsHint": "Configuration snapshots", + "binaryCard": "Binary backups", + "binaryHint": "Recovery images", + "filtersTitle": "Filters", + "filtersSubtitle": "Refine the list by router, type or keyword.", + "searchLabel": "Search", + "searchPlaceholder": "Search by file or router", + "typeLabel": "Type", + "routerLabel": "Router", + "sortLabel": "Sort by", + "orderLabel": "Order", + "allTypes": "All types", + "allRouters": "All routers", + "sortNewest": "Newest", + "sortName": "Name", + "sortRouter": "Router", + "sortType": "Type", + "tableTitle": "Repository table", + "tableSubtitle": "Artifacts available for download, e-mail and restore.", + "compareHint": "Select exactly two .rsc files to compare them.", + "compareSelected": "Compare selected exports", + "fileColumn": "File", + "typeColumn": "Type", + "routerColumn": "Router", + "createdColumn": "Created", + "actionsColumn": "Actions", + "checksum": "Checksum", + "exportType": "Export", + "binaryType": "Binary backup", + "previewDialogTitle": "Export preview", + "diffDialogTitle": "Export diff", + "openHtmlDiff": "Open HTML diff", + "sizeColumn": "Size", + "compareColumn": "Compare", + "compareOlder": "Older file", + "compareNewer": "Newer file", + "pickOlder": "Pick older backup", + "pickNewer": "Pick newer backup", + "compareLatestPair": "Latest pair", + "setOlder": "Set as older", + "setNewer": "Set as newer", + "latestForRouter": "Router diff", + "binaryNoCompare": "Diff available for .rsc only", + "openPlainDiff": "Show plain diff", + "minutesAgo": "{{value}} min ago", + "hoursAgo": "{{value}} h ago", + "daysAgo": "{{value}} d ago", + "compareTitle": "Export comparison", + "compareSubtitle": "Pick two .rsc files and launch the diff without digging through the whole table.", + "exportPoolLabel": "exports ready to compare", + "compareSelectionHint": "Pick an older and a newer file", + "compareReadySameRouter": "Pair ready · router {{router}}", + "compareReadyMixedRouters": "Pair ready · mixed routers" + }, + "settings": { + "title": "Settings", + "eyebrow": "platform configuration", + "subtitle": "Control schedules, retention, notifications, connection tests and shared SSH data.", + "testEmail": "Test e-mail", + "testPushover": "Test Pushover", + "retentionTitle": "Retention", + "retentionSubtitle": "Automatic cleanup windows for files and logs.", + "backupRetentionDays": "Backup retention days", + "logRetentionDays": "Log retention days", + "retentionCron": "Retention cron", + "automationTitle": "Automation", + "automationSubtitle": "Schedules for export, binary jobs, retention and connection checks.", + "enableAutoExport": "Enable auto export", + "enableAutoExportHint": "Run export jobs using the cron rules below.", + "exportCron": "Export cron", + "binaryCron": "Binary cron", + "notificationsTitle": "Notifications", + "notificationsSubtitle": "SMTP and Pushover delivery configuration.", + "smtpEnabled": "SMTP enabled", + "smtpEnabledHint": "Send notifications through the SMTP gateway.", + "failuresOnly": "Failures only", + "failuresOnlyHint": "Limit alerts to failed jobs.", + "smtpHost": "SMTP host", + "smtpPort": "SMTP port", + "smtpLogin": "SMTP login", + "smtpPassword": "SMTP password", + "recipientEmail": "Recipient e-mail", + "pushoverToken": "Pushover token", + "pushoverUserKey": "Pushover user key", + "pushoverTokenPlaceholder": "Application token", + "pushoverUserKeyPlaceholder": "User key", + "sshDefaultsTitle": "SSH defaults", + "sshDefaultsSubtitle": "Optional shared private key used across managed routers.", + "globalSshPrivateKey": "Global SSH private key", + "globalSshPrivateKeyPlaceholder": "Paste PEM or OpenSSH private key", + "save": "Save settings", + "scheduleDisabled": "Disabled", + "scheduleDaily": "Daily", + "scheduleWeekly": "Weekly", + "scheduleCustom": "Custom cron", + "scheduleMode": "Schedule mode", + "scheduleTime": "Time", + "scheduleWeekday": "Weekday", + "weekdayMonday": "Monday", + "weekdayTuesday": "Tuesday", + "weekdayWednesday": "Wednesday", + "weekdayThursday": "Thursday", + "weekdayFriday": "Friday", + "weekdaySaturday": "Saturday", + "weekdaySunday": "Sunday", + "scheduleDisabledHint": "The job will not run automatically.", + "scheduleDailySummary": "Every day at {{time}}", + "scheduleWeeklySummary": "Every {{weekday}} at {{time}}", + "scheduleCustomEmpty": "Enter a custom cron expression", + "statusEnabled": "Enabled", + "statusDisabled": "Disabled", + "noNextRun": "No next run scheduled", + "exportScheduleTitle": "Text exports", + "binaryScheduleTitle": "Binary backups", + "automationPlannerTitle": "Job planner", + "automationPlannerSubtitle": "Every job has its own schedule, so export, binary backup and retention can run in separate windows.", + "automationPlannerTag": "Flexible windows", + "exportPlannerHint": "Decide when readable text exports should be created. Disabled mode stops the automation completely.", + "binaryPlannerHint": "Separate window for full binary backups when you need restore points.", + "retentionPlannerHint": "Retention cleans old backups and logs on its own schedule.", + "connectionTestsTitle": "Automatic connection tests", + "connectionTestsHint": "The application can refresh router status automatically. Set 0 to disable automatic tests.", + "connectionTestIntervalMinutes": "Check every X minutes", + "connectionTestsEverySummary": "Every {{minutes}} minutes", + "connectionTestsDisabledHint": "Automatic connection tests are disabled.", + "sshKeyHelper": "Keep the shared SSH key on the right side. Reveal it only after confirming your account password.", + "sshKeyStoredTag": "Stored key", + "sshKeyWillBeRemovedTag": "Will be removed", + "sshRevealHint": "The current key stays hidden until you confirm your password. You can still paste a new key below to replace it.", + "revealSshPassword": "Current account password", + "revealSshPasswordPlaceholder": "Enter password to reveal the key", + "revealSshKey": "Reveal key", + "hideSshKey": "Hide key", + "clearSshKey": "Clear key", + "sshKeyClearNotice": "The stored shared SSH key will be removed after saving.", + "globalSshPrivateKeyHiddenPlaceholder": "Stored key hidden. Enter the password above to reveal it, or paste a new key here to replace it.", + "sshRevealPasswordRequired": "Enter your current password to reveal the SSH key.", + "sshRevealPasswordInvalid": "The password used to reveal the SSH key is invalid.", + "schedulerAutoExportLabel": "Automatic exports", + "schedulerBinaryLabel": "Binary backups", + "schedulerRetentionLabel": "Retention cleanup", + "schedulerConnectionLabel": "Connection checks", + "schedulerLogsLabel": "Log cleanup", + "schedulerLogsDescription": "Every 24 hours", + "schedulerCronDescription": "{{description}}", + "schedulerInvalidCron": "Invalid cron expression", + "interfaceTitle": "Interface configuration", + "interfaceSubtitle": "Language and typography preferences saved for your account.", + "interfacePreferencesTitle": "Workspace appearance", + "interfacePreferencesHint": "Choose the default language and font family for the whole application.", + "interfacePreferencesTag": "Per-user", + "fontFamily": "Font family", + "fontDefault": "Default" + }, + "logs": { + "title": "Logs", + "eyebrow": "operational history", + "subtitle": "Audit the latest export, restore and maintenance events.", + "daysPlaceholder": "days", + "deleteOlderThan": "Delete older than", + "entriesLabel": "Entries", + "entriesHint": "Loaded rows", + "auditTag": "Audit", + "retentionLabel": "Retention", + "retentionHint": "Cleanup threshold", + "policyTag": "Policy", + "daysSuffix": "days", + "tableTitle": "Log table", + "tableSubtitle": "Chronological list of operations captured by the backend.", + "timestampColumn": "Timestamp", + "messageColumn": "Message", + "retentionInfoLabel": "Configured log retention" + }, + "toast": { + "success": "Done", + "info": "Info", + "error": "Error", + "exportPreviewLoaded": "Export preview loaded.", + "backupSentEmail": "Backup sent by e-mail.", + "binaryUploaded": "Binary backup uploaded to the router.", + "backupDeleted": "Backup deleted.", + "selectedBackupsDeleted": "Selected backups deleted.", + "diffLoaded": "Diff loaded.", + "archivePrepared": "Archive prepared.", + "exportedRouters": "Export completed for {{count}} routers.", + "binaryCompletedRouters": "Binary backup completed for {{count}} routers.", + "routerCreated": "Router created.", + "routerUpdated": "Router updated.", + "routerDeleted": "Router deleted.", + "exportCreated": "Export created.", + "binaryCreated": "Binary backup created.", + "connectionSuccessful": "Connection successful.", + "settingsSaved": "Settings saved.", + "testEmailSent": "Test e-mail sent.", + "testPushoverSent": "Test Pushover notification sent.", + "logsDeletedOlderThan": "Logs older than {{days}} days deleted.", + "passwordChanged": "Password changed.", + "connectionFailed": "Connection test failed.", + "sshKeyUnlocked": "SSH key unlocked.", + "settingsSaveFailed": "Could not save settings.", + "testEmailFailed": "Could not send the test email.", + "testPushoverFailed": "Could not send the test Pushover notification.", + "swosBetaProbeOk": "SwitchOS connectivity verified.", + "swosBetaProbeFailed": "Could not verify SwitchOS access.", + "swosBetaDownloadOk": "SwitchOS backup downloaded.", + "swosBetaDownloadFailed": "Could not download the SwitchOS backup." + }, + "confirm": { + "header": "Confirmation", + "deleteBackup": "Delete this backup file?", + "deleteSelectedFiles": "Delete {{count}} selected files?", + "deleteRouterWithFiles": "Delete the router and all related files?", + "deleteLogsOlderThan": "Delete logs older than {{days}} days?" + }, + "footer": { + "authorLabel": "Author", + "apiLabel": "API", + "apiOnline": "online", + "apiOffline": "offline", + "apiChecking": "checking", + "apiLatencyLabel": "API latency", + "apiDocs": "API docs", + "apiOfflineTitle": "API connection lost", + "apiOfflineMessage": "The backend is not responding. Some features may be temporarily unavailable.", + "retry": "Retry" + }, + "diffConfigs": { + "title": "Config diff", + "eyebrow": "export comparison", + "subtitle": "Dedicated workspace for convenient RouterOS configuration comparisons.", + "exportsCard": "Exports for diff", + "exportsCardHint": ".rsc files in the current scope", + "scopeCard": "Scope", + "scopeCardHint": "Selected router or whole fleet", + "scopeTag": "Scope", + "readyCard": "Pair", + "readyCardHint": "Selection state for comparison", + "readyTag": "State", + "lastDiffCard": "Last diff", + "lastDiffCardHint": "Last opened file pair", + "lastDiffTag": "History", + "workspaceTitle": "Comparison workspace", + "workspaceSubtitle": "Pick a router, set older and newer export, then open the diff in a modal.", + "tableTitle": "Exports to pick from", + "tableSubtitle": "Quickly assign older and newer files and preview them without leaving the page.", + "waitingTag": "Waiting", + "noneSelected": "None" + }, + "switchosBeta": { + "title": "SwitchOS beta", + "eyebrow": "switchos / beta build", + "subtitle": "Standalone module for pulling SwitchOS backups without wiring it into the main repository.", + "betaTag": "Untested beta", + "summaryStandaloneValue": "Standalone", + "summaryStandaloneLabel": "Runs outside the main flow", + "summaryProtocolLabel": "Target protocol", + "summaryArtifactLabel": "Backup format", + "warningTitle": "Module status", + "warningSubtitle": "Separate working path prepared for SwitchOS web scraping.", + "warningHeadline": "This tab is marked as an untested beta build.", + "warningBody": "It does not save devices or files into the existing RouterOS inventory. It is meant for manual access checks and direct SwitchOS backup downloads.", + "formTitle": "Device details", + "formSubtitle": "Enter the switch address and the credentials used in the web UI.", + "label": "File label", + "labelPlaceholder": "for example css326-warehouse", + "host": "Host / URL", + "hostPlaceholder": "for example 192.168.88.1 or http://192.168.88.1", + "port": "Port", + "username": "Username", + "password": "Password", + "passwordPlaceholder": "Leave empty when the device has no password", + "probeButton": "Check access", + "downloadButton": "Download backup .swb", + "resultTitle": "Connection result", + "resultSubtitle": "Quick preview of the device response before downloading the file.", + "resultEmpty": "Check device access first or download the backup right away.", + "baseUrl": "Base URL", + "httpStatus": "HTTP status", + "authMode": "Auth mode", + "pageTitle": "Page title", + "serverHeader": "Server header", + "backupEndpoint": "Backup endpoint", + "available": "Available", + "unavailable": "Unavailable", + "genericError": "The SwitchOS beta operation could not be completed." + } +} diff --git a/frontend/src/assets/i18n/es.json b/frontend/src/assets/i18n/es.json new file mode 100644 index 0000000..8eb28de --- /dev/null +++ b/frontend/src/assets/i18n/es.json @@ -0,0 +1,513 @@ +{ + "app": { + "menu": "Menú" + }, + "sidebar": { + "title": "copia de MikroTik", + "subtitle": "gestor de RouterOS" + }, + "topbar": { + "caption": "mikrotik / centro de control", + "role": "administrador", + "languageSelector": "Selector de idioma" + }, + "common": { + "apply": "Aplicar", + "reset": "Restablecer", + "delete": "Eliminar", + "confirm": "Confirmar", + "cancel": "Cancelar", + "download": "Descargar", + "email": "Enviar correo", + "preview": "Vista previa", + "restore": "Restaurar", + "actions": "Acciones", + "open": "Abrir", + "edit": "Editar", + "diff": "Diff", + "ok": "OK", + "idle": "Sin datos", + "asc": "Ascendente", + "desc": "Descendente", + "enabled": "Activado", + "disabled": "Desactivado", + "failed": "Error" + }, + "nav": { + "dashboard": "Panel", + "routers": "Routers", + "files": "Repositorio", + "settings": "Ajustes", + "logs": "Registros", + "logout": "Cerrar sesión", + "theme": "Tema", + "changePassword": "Cambiar contraseña", + "diffConfigs": "Diff de configuración", + "switchosBeta": "SwitchOS beta" + }, + "auth": { + "username": "Usuario", + "password": "Contraseña", + "login": "Iniciar sesión", + "register": "Registrarse", + "confirmPassword": "Confirmar contraseña", + "changePassword": "Cambiar contraseña", + "currentPassword": "Contraseña actual", + "newPassword": "Nueva contraseña", + "backToLogin": "Volver al inicio de sesión", + "backToApp": "Volver a la app", + "loginSubtitle": "Inicia sesión para continuar.", + "loginFailed": "Error de inicio de sesión", + "accountCreated": "Cuenta creada", + "registrationFailed": "Error de registro", + "passwordsMismatch": "Las contraseñas no coinciden", + "changePasswordFailed": "No se pudo cambiar la contraseña", + "securityEyebrow": "cuenta / seguridad", + "changePasswordSubtitle": "Actualiza la contraseña del administrador sin desorden visual.", + "changePasswordCardSubtitle": "Introduce la contraseña actual y define las nuevas credenciales.", + "passwordPanelSubtitle": "Comprobación rápida de fuerza y coincidencia antes de guardar.", + "passwordStrength": "Fuerza de la contraseña", + "passwordWeak": "Débil", + "passwordMedium": "Media", + "passwordStrong": "Fuerte", + "ruleLength": "Al menos 8 caracteres", + "ruleDigit": "Al menos un número", + "ruleMatch": "Ambos campos coinciden", + "passwordsMatchHint": "La nueva contraseña y la confirmación coinciden." + }, + "dashboard": { + "title": "Panel", + "eyebrow": "inicio / panel", + "subtitle": "Resumen de copias, exportaciones y actividad operativa en un solo lugar.", + "exportAll": "Exportar todo", + "binaryAll": "Copia binaria", + "managedRouters": "Routers", + "managedRoutersHint": "Todos los dispositivos gestionados", + "inventoryTag": "Flota", + "exportsCard": "Exportaciones", + "exportsHint": "Instantáneas legibles de configuración", + "textTag": "Texto", + "binaryCard": "Copias binarias", + "binaryHint": "Puntos de recuperación", + "binaryTag": "Binario", + "allFilesCard": "Todos los archivos", + "allFilesHint": "Artefactos en el repositorio", + "archiveTag": "Archivo", + "storageTitle": "Uso del almacenamiento", + "storageSubtitle": "Resumen actual del uso del repositorio y del espacio libre.", + "folderUsage": "Uso de la carpeta", + "diskUsage": "Uso del disco", + "totalDisk": "Disco total", + "freeSpace": "Espacio libre", + "activityTitle": "Actividad reciente", + "activitySubtitle": "Últimos eventos operativos del backend.", + "noActivity": "No hay eventos recientes para mostrar.", + "avgBackupsPerRouter": "Prom. copias / router", + "activitySuccess": "Tarea completada", + "activityFailure": "Requiere atención", + "activityMaintenance": "Mantenimiento", + "activityDelivery": "Entrega", + "operationsTitle": "Centro de operaciones", + "operationsSubtitle": "Acciones principales e indicadores en vivo del repositorio en un solo lugar.", + "latestSnapshot": "Última instantánea", + "coverageLabel": "Cobertura de la flota", + "coverageHint": "Routers con al menos una copia", + "weeklyActivityLabel": "Actividad de 7 días", + "weeklyActivityHint": "Nuevas copias creadas esta semana", + "busiestRouterLabel": "Router más activo", + "routerSnapshotsHint": "{{count}} instantáneas en el repositorio", + "exportShareLabel": "Cuota de exportaciones", + "activityTodayLabel": "Eventos hoy", + "noneLabel": "Ninguno", + "activityTodayHint": "Entradas creadas hoy", + "usedSpace": "Espacio usado", + "storageViewCapacity": "Capacidad", + "storageViewCapacityHint": "Disco, uso del repositorio y espacio libre en una sola escala.", + "storageViewMix": "Tipos de copias", + "storageViewMixHint": "Distribución de todas las copias entre exportaciones de texto y copias binarias.", + "storageViewActivity": "Actividad 7 días", + "storageViewActivityHint": "Número de nuevas copias creadas en los últimos siete días.", + "storageViewRouters": "Routers principales", + "storageViewRoutersHint": "Dispositivos con mayor número de instantáneas en el repositorio.", + "storageChartEmpty": "Todavía no hay datos suficientes para dibujar este gráfico.", + "storageSnapshotTitle": "Métricas del repositorio", + "storageSnapshotHint": "Vista rápida de los indicadores más importantes de almacenamiento y copias." + }, + "routers": { + "title": "Routers", + "detailTitle": "Detalles del router", + "add": "Añadir router", + "eyebrow": "inventario de dispositivos", + "subtitle": "Gestiona endpoints de RouterOS, credenciales y tareas de copia para toda la flota.", + "registeredDevices": "Dispositivos registrados", + "fleetTag": "Flota", + "sshPassword": "Contraseña SSH", + "passwordHint": "Acceso con contraseña", + "credsTag": "Credenciales", + "sshKey": "Clave SSH", + "keyHint": "Acceso con clave", + "securityTag": "Seguridad", + "defaultPort": "Puerto 22", + "defaultPortHint": "Endpoints SSH estándar", + "portTag": "Puerto", + "listTitle": "Lista de routers", + "listSubtitle": "Vista operativa compacta de todos los dispositivos gestionados.", + "name": "Nombre", + "endpoint": "Endpoint", + "access": "Acceso", + "routerOsTarget": "Objetivo RouterOS", + "passwordMode": "Contraseña", + "noPassword": "Sin contraseña", + "keyMode": "Clave", + "noKey": "Sin clave", + "createDialogTitle": "Añadir router", + "editDialogTitle": "Editar router", + "host": "Host", + "port": "Puerto", + "sshUser": "Usuario SSH", + "sshPrivateKey": "Clave privada SSH", + "optionalPassword": "Contraseña opcional", + "optionalPrivateKey": "Clave privada opcional", + "saveRouter": "Guardar router", + "profileEyebrow": "perfil del router", + "detailSubtitle": "Operaciones del dispositivo e historial de copias", + "exportOne": "Exportar", + "binaryOne": "Binario", + "testConnection": "Probar conexión", + "deleteRouter": "Eliminar router", + "exportsLabel": "Exportaciones", + "exportsLabelHint": "Instantáneas de texto", + "binaryLabel": "Copias binarias", + "binaryLabelHint": "Imágenes de recuperación", + "connectionLabel": "Conexión", + "connectionLabelHint": "Estado de la última prueba automática o manual", + "probeTag": "Prueba", + "accessTag": "Acceso", + "sshUserHint": "Usuario SSH actual", + "deviceStatusTitle": "Estado del dispositivo", + "deviceStatusSubtitle": "Metadatos guardados de la última prueba automática o manual de conexión.", + "connectionStateTitle": "Estado de la conexión", + "lastTestAt": "Última prueba", + "hostname": "Hostname", + "model": "Modelo", + "version": "Versión", + "uptime": "Uptime", + "lastError": "Último error", + "deviceStatusManualHint": "Las comprobaciones automáticas usan el intervalo de ajustes. La prueba manual sigue disponible.", + "noConnection": "Aún no hay una prueba de conexión guardada. Ejecuta una prueba manual o activa las comprobaciones automáticas en ajustes.", + "previewTitle": "Vista previa de exportación", + "previewSubtitle": "Último archivo de exportación abierto.", + "noPreview": "Selecciona un archivo de exportación para ver su contenido.", + "diffTitle": "Último diff", + "diffSubtitle": "Diferencia respecto a la exportación más reciente.", + "exportsTableTitle": "Exportaciones", + "exportsTableSubtitle": "Instantáneas legibles de RouterOS.", + "binaryTableTitle": "Copias binarias", + "binaryTableSubtitle": "Archivos listos para restaurar el dispositivo.", + "summaryKeyAccess": "con acceso por clave", + "summaryPasswordAccess": "con acceso por contraseña", + "previewModalHint": "La última exportación abierta está disponible en un modal.", + "openPreviewModal": "Abrir vista previa", + "diffModalHint": "El último diff cargado está disponible en un modal.", + "openDiffModal": "Abrir diff", + "noDiff": "Elige una exportación y ejecuta un diff para ver la última comparación." + }, + "files": { + "title": "Repositorio", + "eyebrow": "repositorio de artefactos", + "subtitle": "Busca, compara y entrega copias desde una vista clara.", + "downloadZip": "Descargar ZIP", + "visibleFiles": "Archivos visibles", + "visibleFilesHint": "Resultado del filtro actual", + "liveTag": "En vivo", + "selected": "Seleccionados", + "selectedHint": "Listos para acciones masivas", + "batchTag": "Lote", + "exportsCard": "Exportaciones", + "exportsHint": "Instantáneas de configuración", + "binaryCard": "Copias binarias", + "binaryHint": "Imágenes de recuperación", + "filtersTitle": "Filtros", + "filtersSubtitle": "Refina la lista por router, tipo o palabra clave.", + "searchLabel": "Buscar", + "searchPlaceholder": "Buscar por archivo o router", + "typeLabel": "Tipo", + "routerLabel": "Router", + "sortLabel": "Ordenar por", + "orderLabel": "Orden", + "allTypes": "Todos los tipos", + "allRouters": "Todos los routers", + "sortNewest": "Más nuevo", + "sortName": "Nombre", + "sortRouter": "Router", + "sortType": "Tipo", + "tableTitle": "Tabla del repositorio", + "tableSubtitle": "Artefactos disponibles para descarga, correo y restauración.", + "compareHint": "Selecciona exactamente dos archivos .rsc para compararlos.", + "compareSelected": "Comparar exportaciones seleccionadas", + "fileColumn": "Archivo", + "typeColumn": "Tipo", + "routerColumn": "Router", + "createdColumn": "Creado", + "actionsColumn": "Acciones", + "checksum": "Checksum", + "exportType": "Exportación", + "binaryType": "Copia binaria", + "previewDialogTitle": "Vista previa de exportación", + "diffDialogTitle": "Diff de exportación", + "openHtmlDiff": "Abrir diff HTML", + "sizeColumn": "Tamaño", + "compareColumn": "Comparar", + "compareOlder": "Archivo anterior", + "compareNewer": "Archivo más nuevo", + "pickOlder": "Seleccionar copia anterior", + "pickNewer": "Seleccionar copia más nueva", + "compareLatestPair": "Último par", + "setOlder": "Marcar como anterior", + "setNewer": "Marcar como más nuevo", + "latestForRouter": "Diff del router", + "binaryNoCompare": "Diff disponible solo para .rsc", + "openPlainDiff": "Mostrar diff plano", + "minutesAgo": "hace {{value}} min", + "hoursAgo": "hace {{value}} h", + "daysAgo": "hace {{value}} d", + "compareTitle": "Comparación de exportaciones", + "compareSubtitle": "Selecciona dos archivos .rsc y ejecuta el diff sin revisar toda la tabla.", + "exportPoolLabel": "exportaciones listas para comparar", + "compareSelectionHint": "Selecciona un archivo anterior y uno más nuevo", + "compareReadySameRouter": "Par listo · router {{router}}", + "compareReadyMixedRouters": "Par listo · routers mezclados" + }, + "settings": { + "title": "Ajustes", + "eyebrow": "configuración de la plataforma", + "subtitle": "Controla horarios, retención, notificaciones, pruebas de conexión y datos SSH compartidos.", + "testEmail": "Probar correo", + "testPushover": "Probar Pushover", + "retentionTitle": "Retención", + "retentionSubtitle": "Ventanas automáticas de limpieza para archivos y registros.", + "backupRetentionDays": "Días de retención de copias", + "logRetentionDays": "Días de retención de registros", + "retentionCron": "Cron de retención", + "automationTitle": "Automatización", + "automationSubtitle": "Horarios para exportaciones, copias binarias, retención y comprobaciones de conexión.", + "enableAutoExport": "Activar exportación automática", + "enableAutoExportHint": "Ejecuta exportaciones con las reglas cron de abajo.", + "exportCron": "Cron de exportación", + "binaryCron": "Cron binario", + "notificationsTitle": "Notificaciones", + "notificationsSubtitle": "Configuración de entrega SMTP y Pushover.", + "smtpEnabled": "SMTP activado", + "smtpEnabledHint": "Envía notificaciones a través de la pasarela SMTP.", + "failuresOnly": "Solo fallos", + "failuresOnlyHint": "Limita las alertas a trabajos fallidos.", + "smtpHost": "Host SMTP", + "smtpPort": "Puerto SMTP", + "smtpLogin": "Login SMTP", + "smtpPassword": "Contraseña SMTP", + "recipientEmail": "Correo del destinatario", + "pushoverToken": "Token de Pushover", + "pushoverUserKey": "Clave de usuario de Pushover", + "pushoverTokenPlaceholder": "Token de la aplicación", + "pushoverUserKeyPlaceholder": "Clave de usuario", + "sshDefaultsTitle": "Valores SSH por defecto", + "sshDefaultsSubtitle": "Clave privada compartida opcional usada en todos los routers gestionados.", + "globalSshPrivateKey": "Clave privada SSH global", + "globalSshPrivateKeyPlaceholder": "Pega la clave privada PEM u OpenSSH", + "globalSshPrivateKeyHiddenPlaceholder": "La clave guardada está oculta. Introduce la contraseña arriba para verla o pega aquí una nueva clave para reemplazarla.", + "save": "Guardar ajustes", + "scheduleDisabled": "Desactivado", + "scheduleDaily": "Diario", + "scheduleWeekly": "Semanal", + "scheduleCustom": "Cron personalizado", + "scheduleMode": "Modo de horario", + "scheduleTime": "Hora", + "scheduleWeekday": "Día de la semana", + "weekdayMonday": "Lunes", + "weekdayTuesday": "Martes", + "weekdayWednesday": "Miércoles", + "weekdayThursday": "Jueves", + "weekdayFriday": "Viernes", + "weekdaySaturday": "Sábado", + "weekdaySunday": "Domingo", + "scheduleDisabledHint": "La tarea no se ejecutará automáticamente.", + "scheduleDailySummary": "Cada día a las {{time}}", + "scheduleWeeklySummary": "Cada {{weekday}} a las {{time}}", + "scheduleCustomEmpty": "Introduce una expresión cron personalizada", + "statusEnabled": "Activado", + "statusDisabled": "Desactivado", + "noNextRun": "No hay próxima ejecución programada", + "exportScheduleTitle": "Exportaciones de texto", + "binaryScheduleTitle": "Copias binarias", + "automationPlannerTitle": "Planificador de tareas", + "automationPlannerSubtitle": "Cada tarea tiene su propio horario, así que exportación, copia binaria y retención pueden ejecutarse en ventanas separadas.", + "automationPlannerTag": "Ventanas flexibles", + "exportPlannerHint": "Decide cuándo crear exportaciones de texto legibles. El modo desactivado detiene la automatización.", + "binaryPlannerHint": "Ventana separada para copias binarias completas cuando necesitas puntos de restauración.", + "retentionPlannerHint": "La retención limpia copias y registros antiguos según su propio horario.", + "connectionTestsTitle": "Pruebas automáticas de conexión", + "connectionTestsHint": "La aplicación puede actualizar el estado del router automáticamente. Pon 0 para desactivar las pruebas automáticas.", + "connectionTestIntervalMinutes": "Comprobar cada X minutos", + "connectionTestsEverySummary": "Cada {{minutes}} minutos", + "connectionTestsDisabledHint": "Las pruebas automáticas de conexión están desactivadas.", + "sshKeyHelper": "Mantén la clave SSH compartida en la columna derecha. Solo se revela tras confirmar la contraseña de tu cuenta.", + "sshKeyStoredTag": "Clave guardada", + "sshKeyWillBeRemovedTag": "Se eliminará", + "sshRevealHint": "La clave actual permanece oculta hasta que confirmes tu contraseña. También puedes pegar una clave nueva abajo para reemplazarla.", + "revealSshPassword": "Contraseña actual de la cuenta", + "revealSshPasswordPlaceholder": "Introduce la contraseña para revelar la clave", + "revealSshKey": "Revelar clave", + "hideSshKey": "Ocultar clave", + "clearSshKey": "Borrar clave", + "sshKeyClearNotice": "La clave SSH compartida guardada se eliminará al guardar.", + "sshRevealPasswordRequired": "Introduce tu contraseña actual para revelar la clave SSH.", + "sshRevealPasswordInvalid": "La contraseña usada para revelar la clave SSH no es válida.", + "schedulerAutoExportLabel": "Exportaciones automáticas", + "schedulerBinaryLabel": "Copias binarias", + "schedulerRetentionLabel": "Limpieza por retención", + "schedulerConnectionLabel": "Comprobaciones de conexión", + "schedulerLogsLabel": "Limpieza de registros", + "schedulerLogsDescription": "Cada 24 horas", + "schedulerCronDescription": "{{description}}", + "schedulerInvalidCron": "Expresión cron no válida", + "interfaceTitle": "Configuración de la interfaz", + "interfaceSubtitle": "Preferencias de idioma y tipografía guardadas para tu cuenta.", + "interfacePreferencesTitle": "Apariencia del espacio de trabajo", + "interfacePreferencesHint": "Elige el idioma predeterminado y la familia tipográfica para toda la aplicación.", + "interfacePreferencesTag": "Por usuario", + "fontFamily": "Familia tipográfica", + "fontDefault": "Predeterminada" + }, + "logs": { + "title": "Registros", + "eyebrow": "historial operativo", + "subtitle": "Audita los últimos eventos de exportación, restauración y mantenimiento.", + "daysPlaceholder": "días", + "deleteOlderThan": "Eliminar anteriores a", + "entriesLabel": "Entradas", + "entriesHint": "Filas cargadas", + "auditTag": "Auditoría", + "retentionLabel": "Retención", + "retentionHint": "Umbral de limpieza", + "policyTag": "Política", + "daysSuffix": "días", + "tableTitle": "Tabla de registros", + "tableSubtitle": "Lista cronológica de operaciones capturadas por el backend.", + "timestampColumn": "Marca de tiempo", + "messageColumn": "Mensaje", + "retentionInfoLabel": "Retención de registros configurada" + }, + "toast": { + "success": "Hecho", + "info": "Info", + "error": "Error", + "exportPreviewLoaded": "Vista previa de exportación cargada.", + "backupSentEmail": "Copia enviada por correo.", + "binaryUploaded": "Copia binaria subida al router.", + "backupDeleted": "Copia eliminada.", + "selectedBackupsDeleted": "Copias seleccionadas eliminadas.", + "diffLoaded": "Diff cargado.", + "archivePrepared": "Archivo preparado.", + "exportedRouters": "Exportación completada para {{count}} routers.", + "binaryCompletedRouters": "Copia binaria completada para {{count}} routers.", + "routerCreated": "Router creado.", + "routerUpdated": "Router actualizado.", + "routerDeleted": "Router eliminado.", + "exportCreated": "Exportación creada.", + "binaryCreated": "Copia binaria creada.", + "connectionSuccessful": "Conexión correcta.", + "connectionFailed": "La prueba de conexión falló.", + "settingsSaved": "Ajustes guardados.", + "testEmailSent": "Correo de prueba enviado.", + "testPushoverSent": "Notificación de prueba de Pushover enviada.", + "logsDeletedOlderThan": "Se eliminaron los registros anteriores a {{days}} días.", + "passwordChanged": "Contraseña cambiada.", + "sshKeyUnlocked": "Clave SSH desbloqueada.", + "settingsSaveFailed": "No se pudieron guardar los ajustes.", + "testEmailFailed": "No se pudo enviar el correo de prueba.", + "testPushoverFailed": "No se pudo enviar la notificación de prueba de Pushover.", + "swosBetaProbeOk": "Conectividad de SwitchOS verificada.", + "swosBetaProbeFailed": "No se pudo verificar el acceso a SwitchOS.", + "swosBetaDownloadOk": "Backup de SwitchOS descargado.", + "swosBetaDownloadFailed": "No se pudo descargar el backup de SwitchOS." + }, + "confirm": { + "header": "Confirmación", + "deleteBackup": "¿Eliminar este archivo de copia?", + "deleteSelectedFiles": "¿Eliminar {{count}} archivos seleccionados?", + "deleteRouterWithFiles": "¿Eliminar el router y todos los archivos relacionados?", + "deleteLogsOlderThan": "¿Eliminar registros anteriores a {{days}} días?" + }, + "footer": { + "authorLabel": "Autor", + "apiLabel": "API", + "apiOnline": "en línea", + "apiOffline": "sin conexión", + "apiChecking": "comprobando", + "apiLatencyLabel": "Latencia API", + "apiDocs": "Docs API", + "apiOfflineTitle": "Conexión API perdida", + "apiOfflineMessage": "El backend no responde. Algunas funciones pueden no estar disponibles temporalmente.", + "retry": "Reintentar" + }, + "diffConfigs": { + "title": "Diff de configuración", + "eyebrow": "comparación de exportaciones", + "subtitle": "Vista dedicada para comparar configuraciones de RouterOS con mejor UX.", + "exportsCard": "Exportaciones para diff", + "exportsCardHint": "Archivos .rsc en el alcance actual", + "scopeCard": "Alcance", + "scopeCardHint": "Router seleccionado o toda la flota", + "scopeTag": "Alcance", + "readyCard": "Par", + "readyCardHint": "Estado de selección para comparar", + "readyTag": "Estado", + "lastDiffCard": "Último diff", + "lastDiffCardHint": "Último par abierto", + "lastDiffTag": "Historial", + "workspaceTitle": "Espacio de comparación", + "workspaceSubtitle": "Elige un router, define exportación antigua y nueva y abre el diff en un modal.", + "tableTitle": "Exportaciones para elegir", + "tableSubtitle": "Asignación rápida de archivos y vista previa sin salir de la página.", + "waitingTag": "Esperando", + "noneSelected": "Ninguno" + }, + "switchosBeta": { + "title": "SwitchOS beta", + "eyebrow": "switchos / beta", + "subtitle": "Módulo independiente para descargar copias de SwitchOS sin integrarlo con el repositorio principal.", + "betaTag": "Beta sin probar", + "summaryStandaloneValue": "Aislado", + "summaryStandaloneLabel": "Funciona fuera del flujo principal", + "summaryProtocolLabel": "Protocolo objetivo", + "summaryArtifactLabel": "Formato de copia", + "warningTitle": "Estado del módulo", + "warningSubtitle": "Ruta de trabajo separada preparada para scraping web de SwitchOS.", + "warningHeadline": "Esta pestaña está marcada como una beta sin probar.", + "warningBody": "No guarda dispositivos ni archivos en el inventario RouterOS existente. Sirve para comprobar acceso manualmente y descargar el backup de SwitchOS.", + "formTitle": "Datos del dispositivo", + "formSubtitle": "Introduce la dirección del switch y las credenciales usadas en la interfaz web.", + "label": "Etiqueta del archivo", + "labelPlaceholder": "por ejemplo css326-almacen", + "host": "Host / URL", + "hostPlaceholder": "por ejemplo 192.168.88.1 o http://192.168.88.1", + "port": "Puerto", + "username": "Usuario", + "password": "Contraseña", + "passwordPlaceholder": "Déjalo vacío si el equipo no tiene contraseña", + "probeButton": "Comprobar acceso", + "downloadButton": "Descargar backup .swb", + "resultTitle": "Resultado de conexión", + "resultSubtitle": "Vista rápida de la respuesta del equipo antes de descargar el archivo.", + "resultEmpty": "Primero comprueba el acceso al equipo o descarga el backup directamente.", + "baseUrl": "URL base", + "httpStatus": "Código HTTP", + "authMode": "Modo de autenticación", + "pageTitle": "Título de la página", + "serverHeader": "Cabecera del servidor", + "backupEndpoint": "Endpoint del backup", + "available": "Disponible", + "unavailable": "No disponible", + "genericError": "No se pudo completar la operación beta de SwitchOS." + } +} diff --git a/frontend/src/assets/i18n/no.json b/frontend/src/assets/i18n/no.json new file mode 100644 index 0000000..65639b9 --- /dev/null +++ b/frontend/src/assets/i18n/no.json @@ -0,0 +1,513 @@ +{ + "app": { + "menu": "Meny" + }, + "sidebar": { + "title": "MikroTik-backup", + "subtitle": "RouterOS-behandler" + }, + "topbar": { + "caption": "mikrotik / kontrollsenter", + "role": "administrator", + "languageSelector": "Språkvalg" + }, + "common": { + "apply": "Bruk", + "reset": "Tilbakestill", + "delete": "Slett", + "confirm": "Bekreft", + "cancel": "Avbryt", + "download": "Last ned", + "email": "Send e-post", + "preview": "Forhåndsvisning", + "restore": "Gjenopprett", + "actions": "Handlinger", + "open": "Åpne", + "edit": "Rediger", + "diff": "Diff", + "ok": "OK", + "idle": "Ingen data", + "asc": "Stigende", + "desc": "Synkende", + "enabled": "På", + "disabled": "Av", + "failed": "Feilet" + }, + "nav": { + "dashboard": "Dashbord", + "routers": "Rutere", + "files": "Repository", + "settings": "Innstillinger", + "logs": "Logger", + "logout": "Logg ut", + "theme": "Tema", + "changePassword": "Bytt passord", + "diffConfigs": "Konfig-diff", + "switchosBeta": "SwitchOS beta" + }, + "auth": { + "username": "Brukernavn", + "password": "Passord", + "login": "Logg inn", + "register": "Registrer", + "confirmPassword": "Bekreft passord", + "changePassword": "Bytt passord", + "currentPassword": "Nåværende passord", + "newPassword": "Nytt passord", + "backToLogin": "Tilbake til innlogging", + "backToApp": "Tilbake til appen", + "loginSubtitle": "Logg inn for å fortsette.", + "loginFailed": "Innlogging mislyktes", + "accountCreated": "Konto opprettet", + "registrationFailed": "Registrering mislyktes", + "passwordsMismatch": "Passordene samsvarer ikke", + "changePasswordFailed": "Passordbytte mislyktes", + "securityEyebrow": "konto / sikkerhet", + "changePasswordSubtitle": "Oppdater administratorpassordet uten unødvendig visuell støy.", + "changePasswordCardSubtitle": "Skriv inn nåværende passord og sett nye legitimasjonsdata.", + "passwordPanelSubtitle": "Rask kontroll av styrke og samsvar før lagring.", + "passwordStrength": "Passordstyrke", + "passwordWeak": "Svak", + "passwordMedium": "Middels", + "passwordStrong": "Sterk", + "ruleLength": "Minst 8 tegn", + "ruleDigit": "Minst ett tall", + "ruleMatch": "Begge feltene samsvarer", + "passwordsMatchHint": "Det nye passordet og bekreftelsen samsvarer." + }, + "dashboard": { + "title": "Dashbord", + "eyebrow": "hjem / dashbord", + "subtitle": "Oversikt over backuper, eksportfiler og operativ aktivitet på ett sted.", + "exportAll": "Eksporter alle", + "binaryAll": "Binær backup", + "managedRouters": "Rutere", + "managedRoutersHint": "Alle administrerte enheter", + "inventoryTag": "Flåte", + "exportsCard": "Eksporter", + "exportsHint": "Lesbare konfigurasjonsøyeblikksbilder", + "textTag": "Tekst", + "binaryCard": "Binære backuper", + "binaryHint": "Gjenopprettingspunkter", + "binaryTag": "Binær", + "allFilesCard": "Alle filer", + "allFilesHint": "Artefakter i repositoryet", + "archiveTag": "Arkiv", + "storageTitle": "Lagringsbruk", + "storageSubtitle": "Nåværende oversikt over bruk og ledig plass i repositoryet.", + "folderUsage": "Mappebruk", + "diskUsage": "Diskbruk", + "totalDisk": "Total disk", + "freeSpace": "Ledig plass", + "activityTitle": "Nylig aktivitet", + "activitySubtitle": "Siste operative hendelser fra backend.", + "noActivity": "Ingen nylige hendelser å vise.", + "avgBackupsPerRouter": "Snitt backuper / ruter", + "activitySuccess": "Oppgave fullført", + "activityFailure": "Trenger oppmerksomhet", + "activityMaintenance": "Vedlikehold", + "activityDelivery": "Levering", + "operationsTitle": "Driftssenter", + "operationsSubtitle": "Viktigste handlinger og levende repositoryindikatorer på ett sted.", + "latestSnapshot": "Siste øyeblikksbilde", + "coverageLabel": "Flåtedekning", + "coverageHint": "Rutere med minst én backup", + "weeklyActivityLabel": "7-dagers aktivitet", + "weeklyActivityHint": "Nye backuper opprettet denne uken", + "busiestRouterLabel": "Mest aktive ruter", + "routerSnapshotsHint": "{{count}} øyeblikksbilder i repositoryet", + "exportShareLabel": "Eksportandel", + "activityTodayLabel": "Hendelser i dag", + "noneLabel": "Ingen", + "activityTodayHint": "Oppføringer opprettet i dag", + "usedSpace": "Brukt plass", + "storageViewCapacity": "Kapasitet", + "storageViewCapacityHint": "Disk, repositorybruk og ledig plass vist på samme skala.", + "storageViewMix": "Backuptyper", + "storageViewMixHint": "Fordeling av alle kopier mellom teksteksporter og binære backuper.", + "storageViewActivity": "7-dagers aktivitet", + "storageViewActivityHint": "Antall nye backuper opprettet de siste sju dagene.", + "storageViewRouters": "Topp-rutere", + "storageViewRoutersHint": "Enheter med flest øyeblikksbilder i repositoryet.", + "storageChartEmpty": "Det er ikke nok data til å tegne denne grafen ennå.", + "storageSnapshotTitle": "Repository-metrikker", + "storageSnapshotHint": "Rask oversikt over de viktigste lagrings- og backupindikatorene." + }, + "routers": { + "title": "Rutere", + "detailTitle": "Ruterdetaljer", + "add": "Legg til ruter", + "eyebrow": "enhetsinventar", + "subtitle": "Administrer RouterOS-endepunkter, legitimasjon og backupjobber for hele flåten.", + "registeredDevices": "Registrerte enheter", + "fleetTag": "Flåte", + "sshPassword": "SSH-passord", + "passwordHint": "Passordbasert tilgang", + "credsTag": "Tilgang", + "sshKey": "SSH-nøkkel", + "keyHint": "Nøkkelbasert tilgang", + "securityTag": "Sikkerhet", + "defaultPort": "Port 22", + "defaultPortHint": "Standard SSH-endepunkter", + "portTag": "Port", + "listTitle": "Ruterliste", + "listSubtitle": "Kompakt driftsvisning av alle administrerte enheter.", + "name": "Navn", + "endpoint": "Endepunkt", + "access": "Tilgang", + "routerOsTarget": "RouterOS-mål", + "passwordMode": "Passord", + "noPassword": "Ingen passord", + "keyMode": "Nøkkel", + "noKey": "Ingen nøkkel", + "createDialogTitle": "Legg til ruter", + "editDialogTitle": "Rediger ruter", + "host": "Vert", + "port": "Port", + "sshUser": "SSH-bruker", + "sshPrivateKey": "SSH privat nøkkel", + "optionalPassword": "Valgfritt passord", + "optionalPrivateKey": "Valgfri privat nøkkel", + "saveRouter": "Lagre ruter", + "profileEyebrow": "ruterprofil", + "detailSubtitle": "Enhetsoperasjoner og backuphistorikk", + "exportOne": "Eksport", + "binaryOne": "Binær", + "testConnection": "Test tilkobling", + "deleteRouter": "Slett ruter", + "exportsLabel": "Eksporter", + "exportsLabelHint": "Tekstbaserte øyeblikksbilder", + "binaryLabel": "Binære backuper", + "binaryLabelHint": "Gjenopprettingsbilder", + "connectionLabel": "Tilkobling", + "connectionLabelHint": "Status fra siste automatiske eller manuelle test", + "probeTag": "Test", + "accessTag": "Tilgang", + "sshUserHint": "Gjeldende SSH-bruker", + "deviceStatusTitle": "Enhetsstatus", + "deviceStatusSubtitle": "Lagrede metadata fra siste automatiske eller manuelle tilkoblingstest.", + "connectionStateTitle": "Tilkoblingsstatus", + "lastTestAt": "Siste test", + "hostname": "Vertsnavn", + "model": "Modell", + "version": "Versjon", + "uptime": "Oppetid", + "lastError": "Siste feil", + "deviceStatusManualHint": "Automatiske kontroller bruker intervallet fra innstillingene. Manuell test er fortsatt tilgjengelig.", + "noConnection": "Ingen lagret tilkoblingstest ennå. Kjør en manuell test eller aktiver automatiske kontroller i innstillingene.", + "previewTitle": "Forhåndsvisning av eksport", + "previewSubtitle": "Sist åpnet eksportfil.", + "noPreview": "Velg en eksportfil for å se innholdet.", + "diffTitle": "Siste diff", + "diffSubtitle": "Forskjell mot nyeste eksport.", + "exportsTableTitle": "Eksporter", + "exportsTableSubtitle": "Lesbare RouterOS-øyeblikksbilder.", + "binaryTableTitle": "Binære backuper", + "binaryTableSubtitle": "Filer klare for gjenoppretting av enheten.", + "summaryKeyAccess": "med nøkkelbasert tilgang", + "summaryPasswordAccess": "med passordtilgang", + "previewModalHint": "Sist åpnet eksport er tilgjengelig i en modal.", + "openPreviewModal": "Åpne forhåndsvisning", + "diffModalHint": "Sist lastede diff er tilgjengelig i en modal.", + "openDiffModal": "Åpne diff", + "noDiff": "Velg en eksport og kjør diff for å se siste sammenligning." + }, + "files": { + "title": "Repository", + "eyebrow": "artefaktrepository", + "subtitle": "Søk, sammenlign og lever backuper fra én tydelig visning.", + "downloadZip": "Last ned ZIP", + "visibleFiles": "Synlige filer", + "visibleFilesHint": "Resultat av gjeldende filter", + "liveTag": "Live", + "selected": "Valgte", + "selectedHint": "Klare for massehandlinger", + "batchTag": "Batch", + "exportsCard": "Eksporter", + "exportsHint": "Konfigurasjonsøyeblikksbilder", + "binaryCard": "Binære backuper", + "binaryHint": "Gjenopprettingsbilder", + "filtersTitle": "Filtre", + "filtersSubtitle": "Begrens listen etter ruter, type eller nøkkelord.", + "searchLabel": "Søk", + "searchPlaceholder": "Søk etter fil eller ruter", + "typeLabel": "Type", + "routerLabel": "Ruter", + "sortLabel": "Sorter etter", + "orderLabel": "Rekkefølge", + "allTypes": "Alle typer", + "allRouters": "Alle rutere", + "sortNewest": "Nyeste", + "sortName": "Navn", + "sortRouter": "Ruter", + "sortType": "Type", + "tableTitle": "Repositorytabell", + "tableSubtitle": "Artefakter tilgjengelige for nedlasting, e-post og gjenoppretting.", + "compareHint": "Velg nøyaktig to .rsc-filer for å sammenligne dem.", + "compareSelected": "Sammenlign valgte eksporter", + "fileColumn": "Fil", + "typeColumn": "Type", + "routerColumn": "Ruter", + "createdColumn": "Opprettet", + "actionsColumn": "Handlinger", + "checksum": "Checksum", + "exportType": "Eksport", + "binaryType": "Binær backup", + "previewDialogTitle": "Forhåndsvisning av eksport", + "diffDialogTitle": "Eksportdiff", + "openHtmlDiff": "Åpne HTML-diff", + "sizeColumn": "Størrelse", + "compareColumn": "Sammenlign", + "compareOlder": "Eldre fil", + "compareNewer": "Nyere fil", + "pickOlder": "Velg eldre backup", + "pickNewer": "Velg nyere backup", + "compareLatestPair": "Siste par", + "setOlder": "Sett som eldre", + "setNewer": "Sett som nyere", + "latestForRouter": "Ruterdiff", + "binaryNoCompare": "Diff tilgjengelig kun for .rsc", + "openPlainDiff": "Vis ren diff", + "minutesAgo": "{{value}} min siden", + "hoursAgo": "{{value}} t siden", + "daysAgo": "{{value}} d siden", + "compareTitle": "Sammenligning av eksporter", + "compareSubtitle": "Velg to .rsc-filer og start diff uten å grave gjennom hele tabellen.", + "exportPoolLabel": "eksporter klare for sammenligning", + "compareSelectionHint": "Velg en eldre og en nyere fil", + "compareReadySameRouter": "Par klart · ruter {{router}}", + "compareReadyMixedRouters": "Par klart · blandede rutere" + }, + "settings": { + "title": "Innstillinger", + "eyebrow": "plattformkonfigurasjon", + "subtitle": "Styr tidsplaner, retensjon, varsler, tilkoblingstester og delte SSH-data.", + "testEmail": "Test e-post", + "testPushover": "Test Pushover", + "retentionTitle": "Retensjon", + "retentionSubtitle": "Automatiske ryddevinduer for filer og logger.", + "backupRetentionDays": "Dager for backupretensjon", + "logRetentionDays": "Dager for loggretensjon", + "retentionCron": "Retensjons-cron", + "automationTitle": "Automatisering", + "automationSubtitle": "Tidsplaner for eksport, binære jobber, retensjon og tilkoblingskontroller.", + "enableAutoExport": "Aktiver automatisk eksport", + "enableAutoExportHint": "Kjør eksportjobber med cron-reglene nedenfor.", + "exportCron": "Eksport-cron", + "binaryCron": "Binær-cron", + "notificationsTitle": "Varsler", + "notificationsSubtitle": "SMTP- og Pushover-konfigurasjon.", + "smtpEnabled": "SMTP aktivert", + "smtpEnabledHint": "Send varsler via SMTP-gatewayen.", + "failuresOnly": "Kun feil", + "failuresOnlyHint": "Begrens varsler til mislykkede jobber.", + "smtpHost": "SMTP-vert", + "smtpPort": "SMTP-port", + "smtpLogin": "SMTP-innlogging", + "smtpPassword": "SMTP-passord", + "recipientEmail": "Mottaker e-post", + "pushoverToken": "Pushover-token", + "pushoverUserKey": "Pushover-brukernøkkel", + "pushoverTokenPlaceholder": "Applikasjonstoken", + "pushoverUserKeyPlaceholder": "Brukernøkkel", + "sshDefaultsTitle": "SSH-standarder", + "sshDefaultsSubtitle": "Valgfri delt privat nøkkel som brukes på tvers av administrerte rutere.", + "globalSshPrivateKey": "Global SSH privat nøkkel", + "globalSshPrivateKeyPlaceholder": "Lim inn PEM- eller OpenSSH-privat nøkkel", + "globalSshPrivateKeyHiddenPlaceholder": "Den lagrede nøkkelen er skjult. Skriv inn passordet over for å se den, eller lim inn en ny nøkkel her for å erstatte den.", + "save": "Lagre innstillinger", + "scheduleDisabled": "Av", + "scheduleDaily": "Daglig", + "scheduleWeekly": "Ukentlig", + "scheduleCustom": "Egendefinert cron", + "scheduleMode": "Planmodus", + "scheduleTime": "Tid", + "scheduleWeekday": "Ukedag", + "weekdayMonday": "Mandag", + "weekdayTuesday": "Tirsdag", + "weekdayWednesday": "Onsdag", + "weekdayThursday": "Torsdag", + "weekdayFriday": "Fredag", + "weekdaySaturday": "Lørdag", + "weekdaySunday": "Søndag", + "scheduleDisabledHint": "Jobben vil ikke kjøre automatisk.", + "scheduleDailySummary": "Hver dag kl. {{time}}", + "scheduleWeeklySummary": "Hver {{weekday}} kl. {{time}}", + "scheduleCustomEmpty": "Skriv inn et egendefinert cron-uttrykk", + "statusEnabled": "På", + "statusDisabled": "Av", + "noNextRun": "Ingen neste kjøring planlagt", + "exportScheduleTitle": "Teksteksporter", + "binaryScheduleTitle": "Binære backuper", + "automationPlannerTitle": "Jobbplanlegger", + "automationPlannerSubtitle": "Hver jobb har sin egen plan, så eksport, binær backup og retensjon kan kjøre i separate vinduer.", + "automationPlannerTag": "Fleksible vinduer", + "exportPlannerHint": "Bestem når lesbare teksteksporter skal opprettes. Av-modus stopper automatiseringen helt.", + "binaryPlannerHint": "Separat vindu for fulle binære backuper når du trenger gjenopprettingspunkter.", + "retentionPlannerHint": "Retensjon rydder gamle backuper og logger etter sin egen plan.", + "connectionTestsTitle": "Automatiske tilkoblingstester", + "connectionTestsHint": "Appen kan oppdatere ruterstatus automatisk. Sett 0 for å deaktivere automatiske tester.", + "connectionTestIntervalMinutes": "Kontroller hver X minutt", + "connectionTestsEverySummary": "Hvert {{minutes}}. minutt", + "connectionTestsDisabledHint": "Automatiske tilkoblingstester er deaktivert.", + "sshKeyHelper": "Hold den delte SSH-nøkkelen i høyre kolonne. Vis den først etter at du har bekreftet passordet til kontoen din.", + "sshKeyStoredTag": "Lagret nøkkel", + "sshKeyWillBeRemovedTag": "Vil bli fjernet", + "sshRevealHint": "Gjeldende nøkkel forblir skjult til du bekrefter passordet ditt. Du kan også lime inn en ny nøkkel nedenfor for å erstatte den.", + "revealSshPassword": "Gjeldende kontopassord", + "revealSshPasswordPlaceholder": "Skriv inn passord for å vise nøkkelen", + "revealSshKey": "Vis nøkkel", + "hideSshKey": "Skjul nøkkel", + "clearSshKey": "Tøm nøkkel", + "sshKeyClearNotice": "Den lagrede delte SSH-nøkkelen blir fjernet når du lagrer.", + "sshRevealPasswordRequired": "Skriv inn gjeldende passord for å vise SSH-nøkkelen.", + "sshRevealPasswordInvalid": "Passordet som ble brukt for å vise SSH-nøkkelen er ugyldig.", + "schedulerAutoExportLabel": "Automatiske eksporter", + "schedulerBinaryLabel": "Binære backuper", + "schedulerRetentionLabel": "Retensjonsrydding", + "schedulerConnectionLabel": "Tilkoblingskontroller", + "schedulerLogsLabel": "Loggrydding", + "schedulerLogsDescription": "Hver 24. time", + "schedulerCronDescription": "{{description}}", + "schedulerInvalidCron": "Ugyldig cron-uttrykk", + "interfaceTitle": "Grensesnittkonfigurasjon", + "interfaceSubtitle": "Språk- og typografivalg som lagres for kontoen din.", + "interfacePreferencesTitle": "Utseende for arbeidsområdet", + "interfacePreferencesHint": "Velg standardspråk og skriftfamilie for hele applikasjonen.", + "interfacePreferencesTag": "Per bruker", + "fontFamily": "Skriftfamilie", + "fontDefault": "Standard" + }, + "logs": { + "title": "Logger", + "eyebrow": "driftshistorikk", + "subtitle": "Revider de siste eksport-, gjenopprettings- og vedlikeholdshendelsene.", + "daysPlaceholder": "dager", + "deleteOlderThan": "Slett eldre enn", + "entriesLabel": "Oppføringer", + "entriesHint": "Lastede rader", + "auditTag": "Revisjon", + "retentionLabel": "Retensjon", + "retentionHint": "Ryddeterskel", + "policyTag": "Policy", + "daysSuffix": "dager", + "tableTitle": "Loggtabell", + "tableSubtitle": "Kronologisk liste over operasjoner fanget av backend.", + "timestampColumn": "Tidsstempel", + "messageColumn": "Melding", + "retentionInfoLabel": "Konfigurert loggretensjon" + }, + "toast": { + "success": "Ferdig", + "info": "Info", + "error": "Feil", + "exportPreviewLoaded": "Forhåndsvisning av eksport lastet.", + "backupSentEmail": "Backup sendt på e-post.", + "binaryUploaded": "Binær backup lastet opp til ruteren.", + "backupDeleted": "Backup slettet.", + "selectedBackupsDeleted": "Valgte backuper slettet.", + "diffLoaded": "Diff lastet.", + "archivePrepared": "Arkiv klargjort.", + "exportedRouters": "Eksport fullført for {{count}} rutere.", + "binaryCompletedRouters": "Binær backup fullført for {{count}} rutere.", + "routerCreated": "Ruter opprettet.", + "routerUpdated": "Ruter oppdatert.", + "routerDeleted": "Ruter slettet.", + "exportCreated": "Eksport opprettet.", + "binaryCreated": "Binær backup opprettet.", + "connectionSuccessful": "Tilkobling vellykket.", + "connectionFailed": "Tilkoblingstesten mislyktes.", + "settingsSaved": "Innstillinger lagret.", + "testEmailSent": "Test-e-post sendt.", + "testPushoverSent": "Testvarsling for Pushover sendt.", + "logsDeletedOlderThan": "Logger eldre enn {{days}} dager slettet.", + "passwordChanged": "Passord endret.", + "sshKeyUnlocked": "SSH-nøkkel låst opp.", + "settingsSaveFailed": "Kunne ikke lagre innstillingene.", + "testEmailFailed": "Kunne ikke sende test-e-post.", + "testPushoverFailed": "Kunne ikke sende testvarsel via Pushover.", + "swosBetaProbeOk": "SwitchOS-tilkobling verifisert.", + "swosBetaProbeFailed": "Kunne ikke verifisere tilgang til SwitchOS.", + "swosBetaDownloadOk": "SwitchOS-backup lastet ned.", + "swosBetaDownloadFailed": "Kunne ikke laste ned SwitchOS-backup." + }, + "confirm": { + "header": "Bekreftelse", + "deleteBackup": "Slette denne backupfilen?", + "deleteSelectedFiles": "Slette {{count}} valgte filer?", + "deleteRouterWithFiles": "Slette ruteren og alle relaterte filer?", + "deleteLogsOlderThan": "Slette logger eldre enn {{days}} dager?" + }, + "footer": { + "authorLabel": "Forfatter", + "apiLabel": "API", + "apiOnline": "online", + "apiOffline": "offline", + "apiChecking": "sjekker", + "apiLatencyLabel": "API-forsinkelse", + "apiDocs": "API-dokumentasjon", + "apiOfflineTitle": "API-tilkoblingen er borte", + "apiOfflineMessage": "Backend svarer ikke. Noen funksjoner kan være midlertidig utilgjengelige.", + "retry": "Prøv igjen" + }, + "diffConfigs": { + "title": "Konfig-diff", + "eyebrow": "eksportsammenligning", + "subtitle": "Egen side for enklere sammenligning av RouterOS-konfigurasjoner.", + "exportsCard": "Eksporter for diff", + "exportsCardHint": ".rsc-filer i valgt område", + "scopeCard": "Område", + "scopeCardHint": "Valgt ruter eller hele flåten", + "scopeTag": "Område", + "readyCard": "Par", + "readyCardHint": "Valgstatus for sammenligning", + "readyTag": "Status", + "lastDiffCard": "Siste diff", + "lastDiffCardHint": "Sist åpnet filpar", + "lastDiffTag": "Historikk", + "workspaceTitle": "Sammenligningsflate", + "workspaceSubtitle": "Velg ruter, sett eldre og nyere eksport, og åpne diff i modal.", + "tableTitle": "Eksporter å velge fra", + "tableSubtitle": "Rask tildeling av eldre og nyere filer med forhåndsvisning på samme side.", + "waitingTag": "Venter", + "noneSelected": "Ingen" + }, + "switchosBeta": { + "title": "SwitchOS beta", + "eyebrow": "switchos / beta", + "subtitle": "Egen modul for å hente SwitchOS-kopier uten å koble den til hovedlageret.", + "betaTag": "Utestet beta", + "summaryStandaloneValue": "Separat", + "summaryStandaloneLabel": "Kjører utenfor hovedflyten", + "summaryProtocolLabel": "Målprotokoll", + "summaryArtifactLabel": "Backup-format", + "warningTitle": "Modulstatus", + "warningSubtitle": "Separat arbeidsløype forberedt for SwitchOS web scraping.", + "warningHeadline": "Denne fanen er merket som en utestet beta.", + "warningBody": "Den lagrer ikke enheter eller filer i den eksisterende RouterOS-listen. Den er ment for manuell tilgangssjekk og direkte nedlasting av SwitchOS-backup.", + "formTitle": "Enhetsdata", + "formSubtitle": "Oppgi switch-adresse og legitimasjonen som brukes i webgrensesnittet.", + "label": "Filnavn-etikett", + "labelPlaceholder": "for eksempel css326-lager", + "host": "Vert / URL", + "hostPlaceholder": "for eksempel 192.168.88.1 eller http://192.168.88.1", + "port": "Port", + "username": "Brukernavn", + "password": "Passord", + "passwordPlaceholder": "La stå tomt hvis enheten ikke har passord", + "probeButton": "Sjekk tilgang", + "downloadButton": "Last ned backup .swb", + "resultTitle": "Tilkoblingsresultat", + "resultSubtitle": "Rask forhåndsvisning av enhetens svar før filen lastes ned.", + "resultEmpty": "Sjekk tilgang først eller last ned backupen med en gang.", + "baseUrl": "Basis-URL", + "httpStatus": "HTTP-status", + "authMode": "Autentiseringsmodus", + "pageTitle": "Sidetittel", + "serverHeader": "Server-header", + "backupEndpoint": "Backup-endepunkt", + "available": "Tilgjengelig", + "unavailable": "Utilgjengelig", + "genericError": "SwitchOS beta-operasjonen kunne ikke fullføres." + } +} diff --git a/frontend/src/assets/i18n/pl.json b/frontend/src/assets/i18n/pl.json new file mode 100644 index 0000000..fe9d9c2 --- /dev/null +++ b/frontend/src/assets/i18n/pl.json @@ -0,0 +1,513 @@ +{ + "app": { + "menu": "Menu" + }, + "sidebar": { + "title": "kopie MikroTik", + "subtitle": "manager RouterOS" + }, + "topbar": { + "caption": "mikrotik / control center", + "role": "administrator", + "languageSelector": "Wybór języka" + }, + "common": { + "apply": "Zastosuj", + "reset": "Resetuj", + "delete": "Usuń", + "confirm": "Potwierdź", + "cancel": "Anuluj", + "download": "Pobierz", + "email": "Wyślij e-mail", + "preview": "Podgląd", + "restore": "Przywróć", + "actions": "Akcje", + "open": "Otwórz", + "edit": "Edytuj", + "diff": "Diff", + "ok": "OK", + "idle": "Brak", + "asc": "Rosnąco", + "desc": "Malejąco", + "enabled": "Włączone", + "disabled": "Wyłączone", + "failed": "Błąd" + }, + "nav": { + "dashboard": "Dashboard", + "routers": "Routery", + "files": "Repozytorium", + "settings": "Ustawienia", + "logs": "Logi", + "logout": "Wyloguj", + "theme": "Motyw", + "changePassword": "Zmień hasło", + "diffConfigs": "Diff konfiguracji", + "switchosBeta": "SwitchOS beta" + }, + "auth": { + "username": "Użytkownik", + "password": "Hasło", + "login": "Zaloguj", + "register": "Rejestracja", + "confirmPassword": "Potwierdź hasło", + "changePassword": "Zmień hasło", + "currentPassword": "Obecne hasło", + "newPassword": "Nowe hasło", + "backToLogin": "Powrót do logowania", + "backToApp": "Powrót do aplikacji", + "loginSubtitle": "Zaloguj się, aby kontynuować.", + "loginFailed": "Logowanie nie powiodło się", + "accountCreated": "Konto zostało utworzone", + "registrationFailed": "Rejestracja nie powiodła się", + "passwordsMismatch": "Hasła nie są takie same", + "changePasswordFailed": "Zmiana hasła nie powiodła się", + "securityEyebrow": "konto / bezpieczeństwo", + "changePasswordSubtitle": "Zaktualizuj hasło administratora bez zbędnych ustawień dodatkowych.", + "changePasswordCardSubtitle": "Podaj obecne hasło i ustaw nowe dane logowania.", + "passwordPanelSubtitle": "Szybki podgląd siły hasła i zgodności pól przed zapisem.", + "passwordStrength": "Siła hasła", + "passwordWeak": "Słabe", + "passwordMedium": "Średnie", + "passwordStrong": "Mocne", + "ruleLength": "Minimum 8 znaków", + "ruleDigit": "Przynajmniej jedna cyfra", + "ruleMatch": "Oba pola są zgodne", + "passwordsMatchHint": "Nowe hasło i potwierdzenie są zgodne." + }, + "dashboard": { + "title": "Dashboard", + "eyebrow": "strona główna / dashboard", + "subtitle": "Przegląd backupów, eksportów i aktywności operacyjnej w jednym miejscu.", + "exportAll": "Eksportuj wszystko", + "binaryAll": "Backup binarny", + "managedRouters": "Routery", + "managedRoutersHint": "Wszystkie zarządzane urządzenia", + "inventoryTag": "Flota", + "exportsCard": "Eksporty", + "exportsHint": "Czytelne snapshoty konfiguracji", + "textTag": "Tekst", + "binaryCard": "Backupy binarne", + "binaryHint": "Punkty odtworzenia", + "binaryTag": "Binary", + "allFilesCard": "Wszystkie pliki", + "allFilesHint": "Artefakty w repozytorium", + "archiveTag": "Archiwum", + "storageTitle": "Wykorzystanie przestrzeni", + "storageSubtitle": "Bieżący podgląd zajętości repozytorium i wolnego miejsca.", + "folderUsage": "Zajętość katalogu", + "diskUsage": "Użycie dysku", + "totalDisk": "Cały dysk", + "freeSpace": "Wolne miejsce", + "activityTitle": "Ostatnia aktywność", + "activitySubtitle": "Najnowsze zdarzenia operacyjne z backendu.", + "noActivity": "Brak ostatnich zdarzeń do wyświetlenia.", + "avgBackupsPerRouter": "Śr. backupów / router", + "activitySuccess": "Zadanie zakończone", + "activityFailure": "Wymaga uwagi", + "activityMaintenance": "Utrzymanie", + "activityDelivery": "Dystrybucja", + "operationsTitle": "Centrum operacji", + "operationsSubtitle": "Główne akcje i szybkie wskaźniki pracy repozytorium.", + "latestSnapshot": "Najnowszy snapshot", + "coverageLabel": "Pokrycie floty", + "coverageHint": "Routery z co najmniej jednym backupem", + "weeklyActivityLabel": "Aktywność 7 dni", + "weeklyActivityHint": "Nowe backupy z ostatniego tygodnia", + "busiestRouterLabel": "Najaktywniejszy router", + "routerSnapshotsHint": "{{count}} snapshotów w repozytorium", + "exportShareLabel": "Udział eksportów", + "activityTodayLabel": "Zdarzenia dzisiaj", + "noneLabel": "Brak", + "activityTodayHint": "Wpisy z bieżącego dnia", + "usedSpace": "Zajęte miejsce", + "storageViewCapacity": "Pojemność", + "storageViewCapacityHint": "Widok dysku, zajętości repozytorium i wolnego miejsca w jednej skali.", + "storageViewMix": "Typy backupów", + "storageViewMixHint": "Podział wszystkich kopii na eksporty tekstowe i backupy binarne.", + "storageViewActivity": "Aktywność 7 dni", + "storageViewActivityHint": "Liczba nowych backupów z ostatnich siedmiu dni.", + "storageViewRouters": "Top routery", + "storageViewRoutersHint": "Urządzenia z największą liczbą snapshotów w repozytorium.", + "storageChartEmpty": "Brak danych do narysowania wykresu.", + "storageSnapshotTitle": "Metryki repozytorium", + "storageSnapshotHint": "Szybki podgląd najważniejszych wskaźników przestrzeni i backupów." + }, + "routers": { + "title": "Routery", + "detailTitle": "Szczegóły routera", + "add": "Dodaj router", + "eyebrow": "inwentaryzacja urządzeń", + "subtitle": "Zarządzaj endpointami RouterOS, poświadczeniami i zadaniami backupu dla całej floty.", + "registeredDevices": "Zarejestrowane urządzenia", + "fleetTag": "Flota", + "sshPassword": "Hasło SSH", + "passwordHint": "Dostęp hasłem", + "credsTag": "Dostęp", + "sshKey": "Klucz SSH", + "keyHint": "Dostęp kluczem", + "securityTag": "Bezpieczeństwo", + "defaultPort": "Port 22", + "defaultPortHint": "Standardowe endpointy SSH", + "portTag": "Port", + "listTitle": "Lista routerów", + "listSubtitle": "Zwięzły widok operacyjny wszystkich zarządzanych urządzeń.", + "name": "Nazwa", + "endpoint": "Endpoint", + "access": "Dostęp", + "routerOsTarget": "Cel RouterOS", + "passwordMode": "Hasło", + "noPassword": "Bez hasła", + "keyMode": "Klucz", + "noKey": "Bez klucza", + "createDialogTitle": "Dodaj router", + "editDialogTitle": "Edytuj router", + "host": "Host", + "port": "Port", + "sshUser": "Użytkownik SSH", + "sshPrivateKey": "Klucz prywatny SSH", + "optionalPassword": "Opcjonalne hasło", + "optionalPrivateKey": "Opcjonalny klucz prywatny", + "saveRouter": "Zapisz router", + "profileEyebrow": "profil routera", + "detailSubtitle": "Operacje urządzenia i historia backupów", + "exportOne": "Eksport", + "binaryOne": "Backup", + "testConnection": "Test połączenia", + "deleteRouter": "Usuń router", + "exportsLabel": "Eksporty", + "exportsLabelHint": "Tekstowe snapshoty", + "binaryLabel": "Backupy binarne", + "binaryLabelHint": "Obrazy odzyskiwania", + "connectionLabel": "Połączenie", + "connectionLabelHint": "Status z ostatniego automatycznego lub ręcznego testu połączenia", + "probeTag": "Test", + "accessTag": "Dostęp", + "sshUserHint": "Bieżący użytkownik SSH", + "deviceStatusTitle": "Status urządzenia", + "deviceStatusSubtitle": "Zapisane metadane z ostatniego automatycznego lub ręcznego testu połączenia.", + "hostname": "Hostname", + "model": "Model", + "version": "Wersja", + "uptime": "Uptime", + "noConnection": "Brak zapisanego testu połączenia. Uruchom test ręczny albo włącz testy automatyczne w ustawieniach.", + "previewTitle": "Podgląd eksportu", + "previewSubtitle": "Ostatnio otwarty plik eksportu.", + "noPreview": "Wybierz plik eksportu, aby zobaczyć jego zawartość.", + "diffTitle": "Ostatni diff", + "diffSubtitle": "Różnice względem najnowszego eksportu.", + "exportsTableTitle": "Eksporty", + "exportsTableSubtitle": "Czytelne snapshoty RouterOS.", + "binaryTableTitle": "Backupy binarne", + "binaryTableSubtitle": "Pliki do odtworzenia urządzenia.", + "summaryKeyAccess": "z dostępem kluczem", + "summaryPasswordAccess": "z dostępem hasłem", + "connectionStateTitle": "Stan połączenia", + "lastTestAt": "Ostatni test", + "lastError": "Ostatni błąd", + "deviceStatusManualHint": "Automatyczne testy używają interwału z ustawień. Ręczny test nadal jest dostępny.", + "previewModalHint": "Ostatnio otwarty eksport jest dostępny w modalu.", + "openPreviewModal": "Otwórz podgląd", + "diffModalHint": "Ostatnio załadowany diff jest dostępny w modalu.", + "openDiffModal": "Otwórz diff", + "noDiff": "Wybierz eksport i uruchom diff, aby zobaczyć ostatnie porównanie." + }, + "files": { + "title": "Repozytorium", + "eyebrow": "repozytorium artefaktów", + "subtitle": "Szukaj, porównuj i udostępniaj backupy z jednego czytelnego widoku.", + "downloadZip": "Pobierz ZIP", + "visibleFiles": "Widoczne pliki", + "visibleFilesHint": "Wynik bieżącego filtra", + "liveTag": "Live", + "selected": "Zaznaczone", + "selectedHint": "Gotowe do akcji zbiorczych", + "batchTag": "Batch", + "exportsCard": "Eksporty", + "exportsHint": "Snapshoty konfiguracji", + "binaryCard": "Backupy binarne", + "binaryHint": "Obrazy odzyskiwania", + "filtersTitle": "Filtry", + "filtersSubtitle": "Zawęź listę plików po routerze, typie lub słowie kluczowym.", + "searchLabel": "Szukaj", + "searchPlaceholder": "Szukaj po pliku lub routerze", + "typeLabel": "Typ", + "routerLabel": "Router", + "sortLabel": "Sortowanie", + "orderLabel": "Kolejność", + "allTypes": "Wszystkie typy", + "allRouters": "Wszystkie routery", + "sortNewest": "Najnowsze", + "sortName": "Nazwa", + "sortRouter": "Router", + "sortType": "Typ", + "tableTitle": "Tabela repozytorium", + "tableSubtitle": "Artefakty dostępne do pobrania, wysyłki e-mail i przywracania.", + "compareHint": "Zaznacz dokładnie dwa pliki .rsc, aby je porównać.", + "compareSelected": "Porównaj zaznaczone eksporty", + "fileColumn": "Plik", + "typeColumn": "Typ", + "routerColumn": "Router", + "createdColumn": "Utworzono", + "actionsColumn": "Akcje", + "checksum": "Checksum", + "exportType": "Eksport", + "binaryType": "Backup binarny", + "previewDialogTitle": "Podgląd eksportu", + "diffDialogTitle": "Diff eksportów", + "openHtmlDiff": "Otwórz HTML diff", + "sizeColumn": "Rozmiar", + "compareColumn": "Porównanie", + "compareOlder": "Starszy plik", + "compareNewer": "Nowszy plik", + "pickOlder": "Wybierz starszy backup", + "pickNewer": "Wybierz nowszy backup", + "compareLatestPair": "Najnowsza para", + "setOlder": "Ustaw jako starszy", + "setNewer": "Ustaw jako nowszy", + "latestForRouter": "Diff dla routera", + "binaryNoCompare": "Diff tylko dla .rsc", + "openPlainDiff": "Pokaż diff tekstowy", + "minutesAgo": "{{value}} min temu", + "hoursAgo": "{{value}} godz. temu", + "daysAgo": "{{value}} dni temu", + "compareTitle": "Porównanie eksportów", + "compareSubtitle": "Wybierz dwa pliki .rsc i uruchom diff bez przewijania całej tabeli.", + "exportPoolLabel": "eksportów gotowych do porównania", + "compareSelectionHint": "Wybierz starszy i nowszy plik", + "compareReadySameRouter": "Para gotowa · router {{router}}", + "compareReadyMixedRouters": "Para gotowa · różne routery" + }, + "settings": { + "title": "Ustawienia", + "eyebrow": "konfiguracja platformy", + "subtitle": "Skonfiguruj harmonogramy, retencję, powiadomienia, testy połączeń i współdzielone dane SSH.", + "testEmail": "Test e-mail", + "testPushover": "Test Pushover", + "retentionTitle": "Retencja", + "retentionSubtitle": "Okna automatycznego czyszczenia plików i logów.", + "backupRetentionDays": "Dni retencji backupów", + "logRetentionDays": "Dni retencji logów", + "retentionCron": "Cron retencji", + "automationTitle": "Automatyzacja", + "automationSubtitle": "Harmonogramy eksportów, backupów binarnych, retencji i testów połączeń.", + "enableAutoExport": "Włącz auto eksport", + "enableAutoExportHint": "Uruchamiaj zadania eksportu według reguł cron poniżej.", + "exportCron": "Cron eksportu", + "binaryCron": "Cron backupu binarnego", + "notificationsTitle": "Powiadomienia", + "notificationsSubtitle": "Konfiguracja dostarczania SMTP i Pushover.", + "smtpEnabled": "Włącz SMTP", + "smtpEnabledHint": "Wysyłaj powiadomienia przez bramkę SMTP.", + "failuresOnly": "Tylko błędy", + "failuresOnlyHint": "Ogranicz alerty do nieudanych zadań.", + "smtpHost": "Host SMTP", + "smtpPort": "Port SMTP", + "smtpLogin": "Login SMTP", + "smtpPassword": "Hasło SMTP", + "recipientEmail": "E-mail odbiorcy", + "pushoverToken": "Token Pushover", + "pushoverUserKey": "Klucz użytkownika Pushover", + "pushoverTokenPlaceholder": "Token aplikacji", + "pushoverUserKeyPlaceholder": "Klucz użytkownika", + "sshDefaultsTitle": "Domyślne SSH", + "sshDefaultsSubtitle": "Opcjonalny współdzielony klucz prywatny używany przez zarządzane routery.", + "globalSshPrivateKey": "Globalny klucz prywatny SSH", + "globalSshPrivateKeyPlaceholder": "Wklej klucz prywatny PEM lub OpenSSH", + "save": "Zapisz ustawienia", + "scheduleDisabled": "Wyłączony", + "scheduleDaily": "Codziennie", + "scheduleWeekly": "Co tydzień", + "scheduleCustom": "Własny cron", + "scheduleMode": "Tryb harmonogramu", + "scheduleTime": "Godzina", + "scheduleWeekday": "Dzień tygodnia", + "weekdayMonday": "Poniedziałek", + "weekdayTuesday": "Wtorek", + "weekdayWednesday": "Środa", + "weekdayThursday": "Czwartek", + "weekdayFriday": "Piątek", + "weekdaySaturday": "Sobota", + "weekdaySunday": "Niedziela", + "scheduleDisabledHint": "Zadanie nie będzie uruchamiane automatycznie.", + "scheduleDailySummary": "Codziennie o {{time}}", + "scheduleWeeklySummary": "Co {{weekday}} o {{time}}", + "scheduleCustomEmpty": "Wpisz własny cron", + "statusEnabled": "Aktywny", + "statusDisabled": "Wyłączony", + "noNextRun": "Brak zaplanowanego uruchomienia", + "exportScheduleTitle": "Eksporty tekstowe", + "binaryScheduleTitle": "Backupy binarne", + "automationPlannerTitle": "Planer zadań", + "automationPlannerSubtitle": "Każde zadanie ma osobny harmonogram, więc możesz osobno ustawić eksport, backup binarny i retencję.", + "automationPlannerTag": "Elastyczne okna", + "exportPlannerHint": "Ustaw kiedy mają powstawać czytelne eksporty tekstowe. Tryb Wyłączony całkowicie zatrzymuje automat.", + "binaryPlannerHint": "Oddzielne okno dla pełnych backupów binarnych, gdy potrzebujesz punktów odtworzenia.", + "retentionPlannerHint": "Retencja czyści stare backupy i logi według osobnego planu.", + "connectionTestsTitle": "Automatyczne testy połączeń", + "connectionTestsHint": "Aplikacja może sama odświeżać status routerów. Ustaw 0, aby wyłączyć automatyczne testy.", + "connectionTestIntervalMinutes": "Test co X minut", + "connectionTestsEverySummary": "Co {{minutes}} min", + "connectionTestsDisabledHint": "Automatyczne testy połączeń są wyłączone.", + "sshKeyHelper": "Wspólny klucz SSH jest po prawej stronie. Podejrzenie wymaga potwierdzenia hasłem do konta.", + "sshKeyStoredTag": "Klucz zapisany", + "sshKeyWillBeRemovedTag": "Do usunięcia", + "sshRevealHint": "Bieżący klucz pozostaje ukryty, dopóki nie potwierdzisz hasła. Możesz też wkleić nowy klucz poniżej, aby go podmienić.", + "revealSshPassword": "Aktualne hasło do konta", + "revealSshPasswordPlaceholder": "Wpisz hasło, aby podejrzeć klucz", + "revealSshKey": "Pokaż klucz", + "hideSshKey": "Ukryj klucz", + "clearSshKey": "Wyczyść klucz", + "sshKeyClearNotice": "Zapisany wspólny klucz SSH zostanie usunięty po zapisaniu zmian.", + "globalSshPrivateKeyHiddenPlaceholder": "Zapisany klucz jest ukryty. Wpisz hasło powyżej, aby go zobaczyć, albo wklej tutaj nowy klucz, aby go podmienić.", + "sshRevealPasswordRequired": "Wpisz aktualne hasło, aby podejrzeć klucz SSH.", + "sshRevealPasswordInvalid": "Hasło użyte do podejrzenia klucza SSH jest nieprawidłowe.", + "schedulerAutoExportLabel": "Automatyczne eksporty", + "schedulerBinaryLabel": "Backupy binarne", + "schedulerRetentionLabel": "Czyszczenie retencji", + "schedulerConnectionLabel": "Testy połączeń", + "schedulerLogsLabel": "Czyszczenie logów", + "schedulerLogsDescription": "Co 24 godziny", + "schedulerCronDescription": "{{description}}", + "schedulerInvalidCron": "Nieprawidłowe wyrażenie cron", + "interfaceTitle": "Konfiguracja interfejsu", + "interfaceSubtitle": "Preferencje języka i typografii zapisywane dla Twojego konta.", + "interfacePreferencesTitle": "Wygląd przestrzeni roboczej", + "interfacePreferencesHint": "Wybierz domyślny język i rodzinę fontów dla całej aplikacji.", + "interfacePreferencesTag": "Per-user", + "fontFamily": "Rodzina fontów", + "fontDefault": "Domyślna" + }, + "logs": { + "title": "Logi", + "eyebrow": "historia operacyjna", + "subtitle": "Przeglądaj ostatnie zdarzenia eksportu, przywracania i utrzymania.", + "daysPlaceholder": "dni", + "deleteOlderThan": "Usuń starsze niż", + "entriesLabel": "Wpisy", + "entriesHint": "Załadowane rekordy", + "auditTag": "Audyt", + "retentionLabel": "Retencja", + "retentionHint": "Próg czyszczenia", + "policyTag": "Polityka", + "daysSuffix": "dni", + "tableTitle": "Tabela logów", + "tableSubtitle": "Chronologiczna lista operacji zapisanych przez backend.", + "timestampColumn": "Czas", + "messageColumn": "Komunikat", + "retentionInfoLabel": "Ustawiona retencja logów" + }, + "toast": { + "success": "Gotowe", + "info": "Informacja", + "error": "Błąd", + "exportPreviewLoaded": "Załadowano podgląd eksportu.", + "backupSentEmail": "Backup został wysłany e-mailem.", + "binaryUploaded": "Backup binarny został wysłany na router.", + "backupDeleted": "Backup został usunięty.", + "selectedBackupsDeleted": "Wybrane backupy zostały usunięte.", + "diffLoaded": "Załadowano diff.", + "archivePrepared": "Archiwum zostało przygotowane.", + "exportedRouters": "Wykonano eksport dla {{count}} routerów.", + "binaryCompletedRouters": "Wykonano backup binarny dla {{count}} routerów.", + "routerCreated": "Router został dodany.", + "routerUpdated": "Router został zaktualizowany.", + "routerDeleted": "Router został usunięty.", + "exportCreated": "Eksport został utworzony.", + "binaryCreated": "Backup binarny został utworzony.", + "connectionSuccessful": "Połączenie zakończone powodzeniem.", + "settingsSaved": "Ustawienia zostały zapisane.", + "testEmailSent": "Wysłano testowy e-mail.", + "testPushoverSent": "Wysłano testowe powiadomienie Pushover.", + "logsDeletedOlderThan": "Usunięto logi starsze niż {{days}} dni.", + "passwordChanged": "Hasło zostało zmienione.", + "connectionFailed": "Test połączenia nie powiódł się.", + "sshKeyUnlocked": "Klucz SSH został odblokowany.", + "settingsSaveFailed": "Nie udało się zapisać ustawień.", + "testEmailFailed": "Nie udało się wysłać testowego e-maila.", + "testPushoverFailed": "Nie udało się wysłać testowego powiadomienia Pushover.", + "swosBetaProbeOk": "Połączenie ze SwitchOS zostało sprawdzone.", + "swosBetaProbeFailed": "Nie udało się sprawdzić dostępu do SwitchOS.", + "swosBetaDownloadOk": "Backup SwitchOS został pobrany.", + "swosBetaDownloadFailed": "Nie udało się pobrać backupu SwitchOS." + }, + "confirm": { + "header": "Potwierdzenie", + "deleteBackup": "Usunąć ten plik backupu?", + "deleteSelectedFiles": "Usunąć {{count}} zaznaczonych plików?", + "deleteRouterWithFiles": "Usunąć router i wszystkie powiązane pliki?", + "deleteLogsOlderThan": "Usunąć logi starsze niż {{days}} dni?" + }, + "footer": { + "authorLabel": "Autor", + "apiLabel": "API", + "apiOnline": "online", + "apiOffline": "offline", + "apiChecking": "sprawdzanie", + "apiLatencyLabel": "Odpowiedź API", + "apiDocs": "Docs API", + "apiOfflineTitle": "Brak połączenia z API", + "apiOfflineMessage": "Backend nie odpowiada. Część funkcji może być chwilowo niedostępna.", + "retry": "Ponów" + }, + "diffConfigs": { + "title": "Diff konfiguracji", + "eyebrow": "porównanie eksportów", + "subtitle": "Dedykowany widok do wygodnego porównywania konfiguracji RouterOS.", + "exportsCard": "Eksporty do diffu", + "exportsCardHint": "Pliki .rsc w bieżącym zakresie", + "scopeCard": "Zakres", + "scopeCardHint": "Wybrany router lub cała flota", + "scopeTag": "Zakres", + "readyCard": "Para", + "readyCardHint": "Stan wyboru do porównania", + "readyTag": "Stan", + "lastDiffCard": "Ostatni diff", + "lastDiffCardHint": "Ostatnio otwarta para plików", + "lastDiffTag": "Historia", + "workspaceTitle": "Stanowisko porównawcze", + "workspaceSubtitle": "Wybierz router, ustaw starszy i nowszy eksport, a potem otwórz diff w modalu.", + "tableTitle": "Eksporty do wyboru", + "tableSubtitle": "Szybkie przypisanie starszego i nowszego pliku oraz podgląd bez opuszczania strony.", + "waitingTag": "Czeka", + "noneSelected": "Brak" + }, + "switchosBeta": { + "title": "SwitchOS beta", + "eyebrow": "switchos / wersja beta", + "subtitle": "Osobny moduł do pobierania kopii urządzeń SwitchOS bez integracji z głównym repozytorium.", + "betaTag": "Nietestowane beta", + "summaryStandaloneValue": "Osobno", + "summaryStandaloneLabel": "Działa poza głównym obiegiem", + "summaryProtocolLabel": "Protokół docelowy", + "summaryArtifactLabel": "Format kopii", + "warningTitle": "Status modułu", + "warningSubtitle": "To osobna ścieżka robocza przygotowana pod scraping WWW dla SwitchOS.", + "warningHeadline": "Ta zakładka jest oznaczona jako nietestowana wersja beta.", + "warningBody": "Nie zapisuje urządzeń ani plików do istniejącej listy RouterOS. Służy do ręcznego sprawdzenia dostępu i pobrania pliku backupu SwitchOS.", + "formTitle": "Dane urządzenia", + "formSubtitle": "Wprowadź adres przełącznika i dane logowania do panelu WWW.", + "label": "Etykieta pliku", + "labelPlaceholder": "np. css326-magazyn", + "host": "Host / URL", + "hostPlaceholder": "np. 192.168.88.1 albo http://192.168.88.1", + "port": "Port", + "username": "Użytkownik", + "password": "Hasło", + "passwordPlaceholder": "Puste, jeśli urządzenie nie ma hasła", + "probeButton": "Sprawdź dostęp", + "downloadButton": "Pobierz backup .swb", + "resultTitle": "Wynik połączenia", + "resultSubtitle": "Podgląd odpowiedzi urządzenia przed pobraniem pliku.", + "resultEmpty": "Najpierw sprawdź dostęp do urządzenia albo od razu pobierz backup.", + "baseUrl": "Adres bazowy", + "httpStatus": "Kod HTTP", + "authMode": "Tryb autoryzacji", + "pageTitle": "Tytuł strony", + "serverHeader": "Nagłówek serwera", + "backupEndpoint": "Endpoint backupu", + "available": "Dostępny", + "unavailable": "Niedostępny", + "genericError": "Nie udało się wykonać operacji SwitchOS beta." + } +} diff --git a/frontend/src/favicon.ico b/frontend/src/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/index.html b/frontend/src/index.html new file mode 100644 index 0000000..b818b71 --- /dev/null +++ b/frontend/src/index.html @@ -0,0 +1,22 @@ + + + + + RouterOS Backup Manager Next + + + + + + + + diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..712ff48 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,32 @@ +import { HttpClient, provideHttpClient, withInterceptors } from '@angular/common/http'; +import { importProvidersFrom } from '@angular/core'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { provideAnimations } from '@angular/platform-browser/animations'; +import { ConfirmationService, MessageService } from 'primeng/api'; +import { provideRouter } from '@angular/router'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; + +import { AppComponent } from './app/app.component'; +import { routes } from './app/app.routes'; +import { authInterceptor } from './app/core/interceptors/auth.interceptor'; + +export function httpLoaderFactory(http: HttpClient) { + return new TranslateHttpLoader(http, './assets/i18n/', '.json'); +} + +bootstrapApplication(AppComponent, { + providers: [ + provideAnimations(), + provideHttpClient(withInterceptors([authInterceptor])), + provideRouter(routes), + MessageService, + ConfirmationService, + importProvidersFrom( + TranslateModule.forRoot({ + defaultLanguage: 'pl', + loader: { provide: TranslateLoader, useFactory: httpLoaderFactory, deps: [HttpClient] } + }) + ) + ] +}).catch((err) => console.error(err)); diff --git a/frontend/src/styles.css b/frontend/src/styles.css new file mode 100644 index 0000000..5b12ce5 --- /dev/null +++ b/frontend/src/styles.css @@ -0,0 +1,4 @@ +@import './styles/pages.css'; +@import './styles/layout.css'; +@import './styles/auth.css'; +@import './styles/dashboard.css'; diff --git a/frontend/src/styles/auth.css b/frontend/src/styles/auth.css new file mode 100644 index 0000000..42bccf7 --- /dev/null +++ b/frontend/src/styles/auth.css @@ -0,0 +1,165 @@ +.auth-toolbar { + position: fixed; + top: 1rem; + right: 1rem; + display: inline-flex; + align-items: center; + gap: 0.45rem; + z-index: 130; +} + +.auth-toolbar__btn.p-button { + min-width: 2.6rem; + height: 2.4rem; + padding: 0.35rem 0.65rem; + border-color: var(--border-color); + background: color-mix(in srgb, var(--surface-1) 90%, transparent); + color: var(--text-main); +} + +.app-auth-view { + min-height: 100vh; + display: grid; + grid-template-rows: minmax(0, 1fr) auto; +} + +.app-auth-view__content { + min-height: 0; + display: grid; + place-items: center; + padding: 1.5rem; +} + +.app-auth-view__content > * { + width: 100%; + max-width: 720px; +} + +.auth-shell, +.auth-shell--login, +.auth-shell--compact { + position: relative; + display: grid; + align-items: center; + justify-items: stretch; + width: 100%; + min-height: min(100%, 720px); + padding: clamp(4.5rem, 8vh, 5.5rem) 0 1rem; + margin: 0; +} + +.auth-card, +.app-auth-view__content .auth-card--wide { + width: 100%; + min-width: 0; + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: center; + gap: 1.2rem; + padding: 2rem; + border-radius: 24px; + border: 1px solid var(--border-color); + background: linear-gradient(180deg, var(--surface-1) 0%, var(--surface-0) 100%); + box-shadow: var(--shadow-md); +} + +.auth-card__header { + display: grid; + gap: 0.45rem; +} + +.auth-card__header h2 { + margin: 0; + font-size: 1.5rem; + line-height: 1.1; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.auth-card__header p { + margin: 0; + color: var(--text-soft); + font-size: 0.82rem; + line-height: 1.6; +} + +.auth-form { + display: grid; + gap: 1rem; +} + +.auth-form--grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.auth-form--grid > .form-field, +.auth-form--grid > small, +.auth-form--grid > div { + min-width: 0; +} + +.auth-card__actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.auth-card__actions > * { + min-width: 0; +} + +.auth-link { + font-size: 0.78rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--text-soft); + border-bottom: 1px solid transparent; +} + +.auth-link:hover { + color: var(--text-main); + border-bottom-color: currentColor; +} + +.p-button.auth-primary-btn { + width: 100%; + justify-content: center; +} + +.form-field--full { + grid-column: 1 / -1; +} + +.layout-footer--auth { + padding-top: 0.5rem; +} + +@media (max-width: 991px) { + .app-auth-view__content { + align-items: start; + padding: 1rem; + } + + .auth-shell, + .auth-shell--login, + .auth-shell--compact { + min-height: 0; + padding: 4.5rem 0 0.5rem; + } + + .auth-form--grid { + grid-template-columns: 1fr; + } + + .auth-card__actions--split { + flex-direction: column; + align-items: stretch; + } + + .auth-toolbar { + top: 0.9rem; + right: 0.9rem; + } +} diff --git a/frontend/src/styles/dashboard.css b/frontend/src/styles/dashboard.css new file mode 100644 index 0000000..0e26165 --- /dev/null +++ b/frontend/src/styles/dashboard.css @@ -0,0 +1,187 @@ +.dashboard-focus-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1.25rem; + margin-top: 1.5rem; +} + +.dashboard-focus-block { + display: grid; + gap: 1.25rem; +} + +.dashboard-focus-block--pie { + grid-template-columns: minmax(220px, 260px) minmax(0, 1fr); + align-items: center; +} + +.dashboard-focus-pie { + width: min(100%, 240px); + aspect-ratio: 1; + border-radius: 50%; + display: grid; + place-items: center; + margin: 0 auto; +} + +.dashboard-focus-pie__inner { + width: 68%; + aspect-ratio: 1; + border-radius: 50%; + display: grid; + place-items: center; + gap: 0.25rem; + text-align: center; + padding: 1rem; + background: color-mix(in srgb, var(--surface-1) 96%, transparent); + border: 1px solid var(--border-color); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08); +} + +.dashboard-focus-pie__inner strong { + font-family: var(--font-title); + font-size: clamp(1.4rem, 2.6vw, 2rem); + letter-spacing: 0.05em; +} + +.dashboard-focus-pie__inner span { + color: var(--text-soft); + font-size: 0.78rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.dashboard-focus-summary { + display: grid; + gap: 0.75rem; +} + +.dashboard-focus-summary__lead { + margin: 0; + color: var(--text-soft); + line-height: 1.6; +} + +.dashboard-focus-metrics { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.75rem; +} + +.dashboard-focus-metric { + padding: 0.95rem 1rem; + border-radius: 16px; + border: 1px solid var(--border-color); + background: color-mix(in srgb, var(--surface-1) 92%, transparent); + display: grid; + gap: 0.35rem; +} + +.dashboard-focus-metric span { + color: var(--text-soft); + font-size: 0.76rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.dashboard-focus-metric strong { + font-family: var(--font-title); + font-size: 1rem; + letter-spacing: 0.05em; +} + +.dashboard-focus-activity { + display: grid; + gap: 1rem; +} + +.dashboard-focus-activity__summary { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; +} + +.dashboard-focus-activity__summary p { + margin: 0; + color: var(--text-soft); +} + +.dashboard-focus-activity__summary strong { + font-family: var(--font-title); + font-size: 1.5rem; + letter-spacing: 0.06em; +} + +.dashboard-focus-columns { + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + gap: 0.85rem; + align-items: end; + min-height: 220px; +} + +.dashboard-focus-column { + display: grid; + gap: 0.55rem; + justify-items: center; +} + +.dashboard-focus-column small, +.dashboard-focus-column span { + color: var(--text-soft); + font-size: 0.76rem; +} + +.dashboard-focus-column strong { + font-family: var(--font-title); + font-size: 0.9rem; +} + +.dashboard-focus-column__track { + width: 100%; + min-height: 140px; + display: flex; + align-items: end; + justify-content: center; + padding: 0.6rem; + border-radius: 18px; + border: 1px solid var(--border-color); + background: linear-gradient(180deg, color-mix(in srgb, var(--surface-1) 90%, transparent), color-mix(in srgb, var(--surface-0) 94%, transparent)); +} + +.dashboard-focus-column__track span { + width: min(36px, 100%); + border-radius: 999px; + background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 84%, white 8%), color-mix(in srgb, var(--blue) 82%, white 6%)); + min-height: 0; +} + +.dashboard-focus-empty { + min-height: 220px; + display: grid; + place-items: center; +} + +@media (max-width: 1100px) { + .dashboard-focus-grid, + .dashboard-focus-block--pie { + grid-template-columns: 1fr; + } +} + +@media (max-width: 720px) { + .dashboard-focus-metrics { + grid-template-columns: 1fr; + } + + .dashboard-focus-columns { + gap: 0.55rem; + } + + .dashboard-focus-column__track { + min-height: 120px; + padding: 0.45rem; + } +} diff --git a/frontend/src/styles/layout.css b/frontend/src/styles/layout.css new file mode 100644 index 0000000..e94e161 --- /dev/null +++ b/frontend/src/styles/layout.css @@ -0,0 +1,195 @@ + +.layout-shell { + min-height: 100vh; + padding-left: var(--sidebar-width); + transition: padding-left 0.2s ease; +} + +.layout-shell--collapsed { + padding-left: var(--sidebar-collapsed-width); +} + +.layout-shell--collapsed .layout-sidebar { + width: var(--sidebar-collapsed-width); +} + +.layout-main { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.layout-sidebar { + position: fixed; + inset: 0 auto 0 0; + width: var(--sidebar-width); + z-index: 100; + transition: width 0.2s ease, transform 0.2s ease; +} + +.layout-sidebar app-sidebar { + height: 100%; + display: flex; + flex-direction: column; +} + +.layout-content { + flex: 1; + padding: 0 1.5rem 1.5rem; + min-width: 0; +} + +.layout-footer { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 0.85rem 1.2rem; + padding: 0 2rem 1.5rem; +} + +.layout-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + opacity: 0; + visibility: hidden; + z-index: 95; + transition: opacity 0.2s ease; +} + +.layout-overlay.is-visible { + opacity: 1; + visibility: visible; +} + +.topbar { + margin: 0; + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + position: sticky; + top: 0; + z-index: 90; + min-height: 68px; + padding: 0.85rem 1.5rem; + border-bottom: 1px solid rgba(17, 20, 23, 0.1); + backdrop-filter: blur(8px); + background: rgba(255, 255, 255, 0.12); +} + +body.dark-theme .topbar { + border-bottom-color: rgba(146, 170, 194, 0.12); + background: rgba(23, 33, 43, 0.74); +} + +.topbar__right { + display: flex; + align-items: center; + gap: 0.8rem; + min-width: 0; +} + +.topbar__lang-picker, +.auth-toolbar__select-wrap { + position: relative; +} + +.topbar__lang-select, +.auth-toolbar__select { + appearance: none; + -webkit-appearance: none; + min-width: 10.5rem; + min-height: 2.4rem; + padding: 0.55rem 2.15rem 0.55rem 0.95rem; + border-radius: 999px; + border: 1px solid var(--border-color); + background: color-mix(in srgb, var(--surface-1) 92%, transparent); + color: var(--text-main); + font: inherit; + cursor: pointer; +} + +.topbar__lang-picker::after, +.auth-toolbar__select-wrap::after { + content: '\e902'; + font-family: 'primeicons'; + position: absolute; + right: 0.85rem; + top: 50%; + transform: translateY(-50%); + pointer-events: none; + color: var(--text-soft); + font-size: 0.8rem; +} + +.topbar__lang-select option, +.auth-toolbar__select option { + color: #111417; + background: #ffffff; +} + +body.dark-theme .topbar__lang-select, +body.dark-theme .auth-toolbar__select { + background: rgba(255, 255, 255, 0.04); +} + +body.dark-theme .topbar__lang-select option, +body.dark-theme .auth-toolbar__select option { + color: #d6e0e8; + background: #1c2631; +} + +.app-auth-view { + min-height: 100vh; + display: grid; + grid-template-rows: minmax(0, 1fr) auto; +} + +.app-auth-view__content { + min-height: 0; + display: grid; +} + +.app-auth-view__content > * { + min-width: 0; +} + +.layout-footer--auth { + padding-top: 0.5rem; +} + +@media (max-width: 991px) { + .layout-shell, + .layout-shell--collapsed { + padding-left: 0; + } + + .layout-sidebar { + transform: translateX(-100%); + width: min(88vw, var(--sidebar-width)); + } + + .layout-sidebar.is-open { + transform: translateX(0); + } + + .topbar, + .layout-content, + .layout-footer { + padding-left: 1rem; + padding-right: 1rem; + } + + .topbar { + flex-direction: column; + align-items: flex-start; + } + + .topbar__right { + width: 100%; + justify-content: space-between; + flex-wrap: wrap; + } +} diff --git a/frontend/src/styles/pages.css b/frontend/src/styles/pages.css new file mode 100644 index 0000000..b7a0e15 --- /dev/null +++ b/frontend/src/styles/pages.css @@ -0,0 +1,3412 @@ +:root{ + --bg-page: #ecece8; + --bg-grid: radial-gradient(circle at 10% 10%, rgba(159, 115, 84, 0.08), transparent 24%), radial-gradient(circle at 85% 14%, rgba(19, 24, 28, 0.05), transparent 18%); + --surface-0: rgba(250, 250, 247, 0.82); + --surface-1: #f8f8f5; + --surface-2: #f1f1ed; + --surface-3: #dfdfd8; + --surface-4: #d0d0c8; + --text-main: #111417; + --text-soft: #5e666e; + --text-faint: #7b848d; + --border-color: rgba(17, 20, 23, 0.12); + --border-strong: rgba(17, 20, 23, 0.2); + --primary: #101316; + --primary-soft: #49535c; + --accent: #8d593a; + --accent-2: #0c6a50; + --blue: #285c9d; + --success: #0d8a63; + --warning: #b67a21; + --danger: #c24646; + --shadow-lg: 0 24px 60px rgba(17, 20, 23, 0.08); + --shadow-md: 0 12px 30px rgba(17, 20, 23, 0.05); + --radius-xl: 28px; + --radius-lg: 20px; + --radius-md: 14px; + --sidebar-width: 260px; + --sidebar-collapsed-width: 92px; + --topbar-height: 76px; + --font-body: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-title: "Roboto Mono", "IBM Plex Mono", "SFMono-Regular", Consolas, monospace; +} + +body.dark-theme{ + --bg-page: #17212b; + --bg-grid: linear-gradient(180deg, rgba(91, 119, 145, 0.05), transparent 32%), radial-gradient(circle at 18% 12%, rgba(63, 130, 196, 0.08), transparent 18%); + --surface-0: rgba(29, 39, 51, 0.9); + --surface-1: #1d2733; + --surface-2: #222d3a; + --surface-3: #2d3947; + --surface-4: #3a4858; + --text-main: #dae4ec; + --text-soft: #93a5b6; + --text-faint: #708295; + --border-color: rgba(146, 170, 194, 0.16); + --border-strong: rgba(146, 170, 194, 0.25); + --primary: #edf2f7; + --primary-soft: #bccada; + --accent: #4b90d9; + --accent-2: #4fb593; + --blue: #4b90d9; + --success: #4fb593; + --warning: #f0b45b; + --danger: #e37a7a; + --shadow-lg: 0 22px 48px rgba(0, 0, 0, 0.24); + --shadow-md: 0 10px 24px rgba(0, 0, 0, 0.18); +} + +*{ + box-sizing: border-box; +} + +html, body{ + min-height: 100%; +} + +body{ + margin: 0; + color: var(--text-main); + font-family: var(--font-body); + background: var(--bg-page); + background-image: var(--bg-grid); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body::before{ + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + background-image: linear-gradient(rgba(17, 20, 23, 0.018) 1px, transparent 1px), linear-gradient(90deg, rgba(17, 20, 23, 0.018) 1px, transparent 1px); + background-size: 28px 28px; + opacity: 0.28; +} + +body.dark-theme::before{ + background-image: linear-gradient(rgba(218, 228, 236, 0.02) 1px, transparent 1px), linear-gradient(90deg, rgba(218, 228, 236, 0.02) 1px, transparent 1px); + opacity: 0.15; +} + +body, .p-component, .p-inputtext, .p-dropdown, .p-button, .p-card, .p-tag, .p-inputswitch, .p-table{ + font-family: var(--font-body); +} + +pre, code, .page-header__title, .dashboard-hero__title, .topbar__headline, .sidebar-brand__text h2, .stat-card__value, .stat-card__label, .section-card__title, .hero-spec strong, .hero-terminal__tabs, .runbook-item strong, .table-primary{ + font-family: var(--font-title); +} + +img{ + max-width: 100%; +} + +a{ + color: inherit; + text-decoration: none; +} + +small{ + color: var(--text-soft); +} + +.layout-shell{ + min-height: 100vh; + padding-left: var(--sidebar-width); + transition: padding-left 0.2s ease; +} + +.layout-shell--collapsed{ + padding-left: var(--sidebar-collapsed-width); +} + +.layout-sidebar{ + position: fixed; + left: 0; + top: 0; + bottom: 0; + width: var(--sidebar-width); + background: linear-gradient(180deg, #151d26 0%, #19222d 100%); + border-right: 1px solid rgba(255, 255, 255, 0.08); + color: #d7e1eb; + padding: 1.15rem 1rem; + z-index: 100; + transition: width 0.2s ease, transform 0.2s ease; + overflow: hidden; +} + +.layout-shell--collapsed .layout-sidebar{ + width: var(--sidebar-collapsed-width); +} + +.layout-main{ + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.layout-content{ + flex: 1; + padding: 0 2rem 2rem; +} + +.layout-footer{ + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 0.85rem 1.2rem; + padding: 0 2rem 1.5rem; + color: var(--text-faint); + font-size: 0.74rem; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.layout-overlay{ + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + opacity: 0; + visibility: hidden; + z-index: 95; + transition: opacity 0.2s ease; +} + +.layout-overlay.is-visible{ + opacity: 1; + visibility: visible; +} + +.topbar{ + min-height: var(--topbar-height); + margin: 0; + padding: 1rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + border-bottom: 1px solid var(--border-color); + background: rgba(255, 255, 255, 0.18); + backdrop-filter: blur(10px); + position: sticky; + top: 0; + z-index: 90; +} + +body.dark-theme .topbar{ + background: rgba(23, 33, 43, 0.82); +} + +.topbar__left, .topbar__right, .header-actions-row, .table-actions, .inline-tags, .filters-actions, .dialog-actions, .hero-actions, .sidebar-brand, .sidebar-footer__card, .activity-item, .runbook-item, .topbar__user, .form-switch-row, .metrics-stack{ + display: flex; + align-items: center; + gap: 0.8rem; +} + +.topbar__left, .topbar__right, .header-actions-row, .table-actions, .inline-tags, .filters-actions, .dialog-actions, .topbar__caption, .page-header__eyebrow, .sidebar-section__label, .stat-card__label, .hero-spec span, .dashboard-hero__eyebrow, .hero-terminal small, .layout-footer span, .sidebar-footer__title{ + font-size: 0.72rem; + letter-spacing: 0.16em; + text-transform: uppercase; +} + +.topbar__caption, .page-header__eyebrow, .sidebar-section__label, .layout-footer, .sidebar-footer__text, .stat-card__hint, .section-card__subtitle, .page-header__subtitle, .dashboard-hero__text, .metric-tile span, .metric-row span, .activity-item__message + small, .table-secondary, .form-field label, .topbar__user-meta small{ + color: var(--text-soft); +} + +.topbar__headline{ + font-size: 1.02rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.topbar__status{ + display: inline-flex; + align-items: center; + gap: 0.55rem; + padding: 0.55rem 0.85rem; + border: 1px solid var(--border-color); + border-radius: 999px; + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.14em; + background: var(--surface-1); +} + +.status-dot, .activity-item__dot{ + width: 0.65rem; + height: 0.65rem; + border-radius: 999px; + background: var(--success); + box-shadow: 0 0 0 4px rgba(79, 181, 147, 0.12); +} + +.topbar__user-meta{ + display: flex; + flex-direction: column; + line-height: 1.2; +} + +.topbar__action-btn.p-button, .topbar__logout-btn.p-button, .topbar__menu-btn.p-button{ + min-width: auto; +} + +.sidebar-brand{ + align-items: center; + padding-bottom: 1.25rem; + margin-bottom: 1rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.sidebar-brand__logo{ + width: 42px; + height: 42px; + border: 1px solid rgba(255, 255, 255, 0.24); + border-radius: 10px; + display: grid; + place-items: center; + font-family: var(--font-title); + font-size: 0.86rem; + letter-spacing: 0.12em; + color: #fff; +} + +.sidebar-brand__text h2, .sidebar-brand__text p, .sidebar-section__label, .sidebar-footer__title, .sidebar-footer__text, .page-header__title, .page-header__subtitle, .section-card__title, .section-card__subtitle, .stat-card__label, .stat-card__value, .stat-card__hint, .metric-tile span, .metric-row span, .activity-item__message, .form-field label, .dashboard-hero__title, .dashboard-hero__text, .hero-terminal__tabs, .hero-spec strong, .hero-spec span{ + margin: 0; +} + +.sidebar-brand__text h2{ + font-size: 1.02rem; + color: #fff; + text-transform: lowercase; + letter-spacing: 0.18em; +} + +.sidebar-brand__text p, .sidebar-footer__text, .sidebar-section__label{ + color: rgba(215, 225, 235, 0.64); +} + +.sidebar-section{ + padding: 0.25rem 0 0.5rem; +} + +.sidebar-nav{ + display: grid; + gap: 0.35rem; +} + +.sidebar-nav__item{ + display: flex; + align-items: center; + gap: 0.85rem; + min-height: 44px; + padding: 0.75rem 0.85rem; + border-radius: 12px; + color: rgba(230, 238, 245, 0.84); + border: 1px solid transparent; + transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease; +} + +.sidebar-nav__item:hover{ + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.08); +} + +.sidebar-nav__item.is-active{ + background: rgba(75, 144, 217, 0.18); + border-color: rgba(75, 144, 217, 0.42); + color: #fff; +} + +.sidebar-nav__item i{ + font-size: 1rem; + width: 1rem; +} + +.sidebar-footer{ + margin-top: auto; + padding-top: 1rem; +} + +.sidebar-footer__card{ + flex-direction: column; + align-items: flex-start; + padding: 1rem; + border-radius: 16px; + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.layout-sidebar app-sidebar{ + height: 100%; + display: flex; + flex-direction: column; +} + +.page-header{ + display: flex; + justify-content: space-between; + align-items: flex-end; + gap: 1rem; + padding: 2rem 0 1.25rem; + border-bottom: 1px solid var(--border-color); + margin-bottom: 1.5rem; +} + +.page-header__title{ + font-size: clamp(2rem, 4vw, 3.2rem); + line-height: 1; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.page-header__subtitle{ + max-width: 56rem; + margin-top: 0.9rem; + font-size: 0.98rem; + line-height: 1.7; +} + +.stats-grid, .dashboard-grid, .metric-grid-2, .filters-grid, .two-column-grid, .form-grid-2, .settings-grid{ + display: grid; + gap: 1rem; +} + +.stats-grid{ + grid-template-columns: repeat(4, minmax(0, 1fr)); + margin-bottom: 1rem; +} + +.compact-grid{ + margin-bottom: 1rem; +} + +.dashboard-grid, .two-column-grid, .settings-grid{ + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.metric-grid-2, .form-grid-2{ + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.filters-grid{ + grid-template-columns: 1.3fr repeat(4, minmax(0, 1fr)) auto; + align-items: end; +} + +.stacked-sections{ + display: grid; + gap: 1rem; +} + +.section-card.p-card, .stat-card.p-card{ + border-radius: 20px; + border: 1px solid var(--border-color); + background: linear-gradient(180deg, var(--surface-1) 0%, var(--surface-0) 100%); + box-shadow: var(--shadow-md); + overflow: hidden; +} + +.section-card .p-card-body, .stat-card .p-card-body{ + padding: 0; +} + +.section-card .p-card-content, .stat-card .p-card-content{ + padding: 1.2rem 1.25rem; +} + +.section-card__header{ + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: flex-start; + padding-bottom: 1rem; + margin-bottom: 1rem; + border-bottom: 1px solid var(--border-color); +} + +.section-card__title{ + font-size: 1.05rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.section-card__subtitle{ + margin-top: 0.35rem; + line-height: 1.6; +} + +.stat-card{ + position: relative; +} + +.stat-card::before{ + content: ""; + position: absolute; + left: 0; + right: 0; + top: 0; + height: 4px; + background: linear-gradient(90deg, var(--accent), var(--blue)); +} + +.stat-card__row{ + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: flex-start; + margin-bottom: 1rem; +} + +.stat-card__label{ + margin-bottom: 0.55rem; +} + +.stat-card__value{ + font-size: clamp(1.6rem, 3vw, 2.1rem); + line-height: 1; + letter-spacing: 0.04em; +} + +.stat-card__hint{ + margin-top: 0.6rem; + font-size: 0.92rem; +} + +.stat-card__icon{ + width: 48px; + height: 48px; + border-radius: 14px; + display: grid; + place-items: center; + border: 1px solid var(--border-color); + background: var(--surface-2); + font-size: 1.05rem; +} + +.icon-blue{ color: var(--blue); } +.icon-emerald{ color: var(--success); } +.icon-amber{ color: var(--warning); } +.icon-violet{ color: var(--accent); } + +body.dark-theme .settings-actions{ + display: flex; + flex-direction: column; +} + +body.dark-theme .metric-tile, .empty-state{ + justify-content: space-between; +} + +.metric-grid-2{ + margin-top: 0.2rem; +} + +.metric-tile{ + padding: 1rem; + border: 1px solid var(--border-color); + border-radius: 16px; + background: var(--surface-2); +} + +.metric-tile strong, .metric-row strong{ + font-family: var(--font-title); + font-size: 1rem; + letter-spacing: 0.06em; +} + +.table-primary{ + font-size: 0.96rem; + letter-spacing: 0.04em; +} + +.code-preview{ + margin: 0; + padding: 1rem; + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.06); + background: #17212b; + color: #dae4ec; + overflow: auto; + line-height: 1.6; + font-size: 0.86rem; +} + +.form-field{ + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.form-field label{ + font-size: 0.8rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.form-field--full{ + grid-column: 1 / -1; +} + +.form-switch-row{ + align-items: center; + min-height: 48px; + padding: 0.35rem 0; +} + +.form-error, .form-success{ + font-size: 0.88rem; +} + +.form-error{ color: var(--danger); } +.form-success{ color: var(--success); } + +.p-button{ + border-radius: 12px; + border-width: 1px; + font-size: 0.82rem; + font-weight: 600; + letter-spacing: 0.14em; + text-transform: uppercase; + padding: 0.8rem 1rem; + background: var(--primary); + border-color: var(--primary); + color: #fff; + box-shadow: none; +} + +.p-button:not(.p-disabled):hover{ + filter: brightness(1.06); +} + +.p-button.p-button-secondary, .p-button.p-button-help, .p-button.p-button-outlined, .p-button.p-button-text{ + background: transparent; + color: var(--text-main); + border-color: var(--border-strong); +} + +.p-button.p-button-text{ + border-color: transparent; +} + +body.dark-theme .p-button.p-button-secondary, body.dark-theme .p-button.p-button-help, body.dark-theme .p-button.p-button-outlined, body.dark-theme .p-button.p-button-text{ + color: var(--text-main); +} + +.p-button.p-button-danger{ + background: transparent; + border-color: rgba(194, 70, 70, 0.5); + color: var(--danger); +} + +.p-inputtext, .p-dropdown, .p-inputtextarea, textarea.p-inputtextarea{ + width: 100%; + border-radius: 12px; + border: 1px solid var(--border-strong); + background: rgba(255, 255, 255, 0.5); + color: var(--text-main); + padding: 0.82rem 0.9rem; + box-shadow: none; +} + +body.dark-theme .p-inputtext, body.dark-theme .p-dropdown, body.dark-theme .p-inputtextarea, body.dark-theme textarea.p-inputtextarea{ + background: rgba(255, 255, 255, 0.03); +} + +.p-dropdown .p-dropdown-label{ + padding: 0; +} + +.p-dropdown .p-dropdown-trigger{ + width: 2.6rem; +} + +.p-inputtext:enabled:focus, .p-dropdown:not(.p-disabled).p-focus, .p-inputtextarea:enabled:focus, textarea.p-inputtextarea:enabled:focus{ + border-color: var(--accent); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent); +} + +.p-input-icon-left{ + position: relative; + display: block; +} + +.p-input-icon-left > i{ + left: 0.9rem; + color: var(--text-soft); +} + +.p-input-icon-left > .p-inputtext{ + padding-left: 2.45rem; +} + +.p-inputswitch{ + width: 3.5rem; + height: 1.8rem; +} + +.p-inputswitch .p-inputswitch-slider{ + background: var(--surface-3); + border: 1px solid var(--border-strong); + border-radius: 999px; +} + +.p-inputswitch .p-inputswitch-slider::before{ + width: 1.2rem; + height: 1.2rem; + left: 0.22rem; + margin-top: -0.6rem; + border-radius: 50%; + background: var(--surface-1); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.18); +} + +.p-inputswitch.p-inputswitch-checked .p-inputswitch-slider{ + background: color-mix(in srgb, var(--accent) 80%, #fff 20%); +} + +body.dark-theme .p-inputswitch.p-inputswitch-checked .p-inputswitch-slider{ + background: var(--blue); +} + +.p-tag{ + border-radius: 999px; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.p-progressbar{ + height: 0.65rem; + border-radius: 999px; + background: var(--surface-3); +} + +.p-progressbar .p-progressbar-value{ + background: linear-gradient(90deg, var(--accent), var(--blue)); +} + +.app-table{ + border: 1px solid var(--border-color); + border-radius: 18px; + overflow: hidden; +} + +.app-table .p-datatable-wrapper{ + border-radius: 18px; +} + +.app-table .p-datatable-table{ + min-width: 100%; +} + +.app-table .p-datatable-thead > tr > th{ + background: var(--surface-2); + color: var(--text-soft); + border-bottom: 1px solid var(--border-color); + font-size: 0.76rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + padding: 0.95rem 1rem; +} + +.app-table .p-datatable-tbody > tr{ + background: transparent; +} + +.app-table .p-datatable-tbody > tr > td{ + padding: 0.95rem 1rem; + border-bottom: 1px solid var(--border-color); +} + +.app-table .p-datatable-tbody > tr:hover > td{ + background: rgba(255, 255, 255, 0.35); +} + +body.dark-theme .app-table .p-datatable-thead > tr > th{ + background: #202a36; +} + +body.dark-theme .app-table .p-datatable-tbody > tr:hover > td{ + background: rgba(255, 255, 255, 0.03); +} + +.table-secondary{ + display: inline-block; + margin-top: 0.3rem; +} + +.p-checkbox .p-checkbox-box{ + border-radius: 8px; + border-color: var(--border-strong); + background: transparent; +} + +.p-checkbox:not(.p-checkbox-disabled) .p-checkbox-box:hover, .p-checkbox .p-checkbox-box.p-highlight{ + border-color: var(--accent); + background: color-mix(in srgb, var(--accent) 84%, #fff 16%); +} + +.p-paginator{ + border-top: 1px solid var(--border-color); + background: var(--surface-1); +} + +.p-dropdown-panel, .p-component-overlay{ + backdrop-filter: blur(10px); +} + +.p-dropdown-panel .p-dropdown-items{ + padding: 0.35rem; +} + +.p-dropdown-panel .p-dropdown-item{ + border-radius: 10px; +} + +.p-avatar{ + background: var(--primary); + color: #fff; + font-family: var(--font-title); +} + + +.p-chip{ + background: rgba(255, 255, 255, 0.08); + color: inherit; + border: 1px solid rgba(255, 255, 255, 0.12); +} + +.p-dialog{ + border-radius: 20px; + overflow: hidden; + border: 1px solid var(--border-color); +} + +.p-dialog .p-dialog-header, .p-dialog .p-dialog-content, .p-dialog .p-dialog-footer{ + background: var(--surface-1); + color: var(--text-main); +} + +.p-dialog .p-dialog-header{ + border-bottom: 1px solid var(--border-color); +} + +.p-dialog .p-dialog-footer{ + border-top: 1px solid var(--border-color); +} + +.p-dropdown-panel, .p-multiselect-panel{ + background: var(--surface-1); + border: 1px solid var(--border-color); + color: var(--text-main); +} + +.p-dropdown-panel .p-dropdown-item.p-highlight, .p-dropdown-panel .p-dropdown-item:hover{ + background: var(--surface-2); + color: var(--text-main); +} + +.empty-state, .compact-empty{ + padding: 1.2rem; + border: 1px dashed var(--border-strong); + border-radius: 14px; + color: var(--text-soft); + background: var(--surface-2); +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +.settings-actions{ + justify-content: flex-end; + grid-column: 1 / -1; +} + +@media (max-width: 1300px) { + .stats-grid{ + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .filters-grid{ + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .dashboard-grid{ + grid-template-columns: 1fr; + } +} + +@media (max-width: 991px) { + .layout-shell, .layout-shell--collapsed{ + padding-left: 0; + } + + .layout-sidebar{ + transform: translateX(-100%); + width: min(88vw, var(--sidebar-width)); + } + + .layout-sidebar.is-open{ + transform: translateX(0); + } + + .topbar, .layout-content, .layout-footer{ + padding-left: 1rem; + padding-right: 1rem; + } + + .page-header{ + align-items: flex-start; + } +} + +@media (max-width: 720px) { + .page-header, .topbar, .filters-grid, .stats-grid, .metric-grid-2, .form-grid-2{ + grid-template-columns: 1fr; + } + + .topbar, .page-header{ + flex-direction: column; + align-items: flex-start; + } + + .topbar__right{ + width: 100%; + } + + + + + +} + +/* v6 refinements */ +.layout-content{ + padding-bottom: 2.5rem; +} + +.topbar{ + min-height: 72px; + padding-top: 0.9rem; + padding-bottom: 0.9rem; + background: rgba(249, 249, 246, 0.86); + border-bottom-color: rgba(17, 20, 23, 0.1); +} + +body.dark-theme .topbar{ + background: rgba(21, 29, 38, 0.92); + border-bottom-color: rgba(146, 170, 194, 0.12); +} + +.topbar__caption{ + letter-spacing: 0.18em; +} + +.topbar__headline{ + font-family: var(--font-title); + font-size: 1rem; + letter-spacing: 0.1em; +} + +.topbar__icon-btn.p-button, .topbar__logout-btn.p-button{ + min-width: 2.75rem; + height: 2.75rem; +} + +.topbar__icon-btn.p-button{ + border: 1px solid var(--border-color); + background: var(--surface-1); +} + +body.dark-theme .topbar__icon-btn.p-button{ + background: rgba(255, 255, 255, 0.03); +} + +.topbar__logout-btn.p-button{ + padding-inline: 1rem; +} + +.sidebar-brand__logo{ + border-radius: 12px; + background: rgba(255, 255, 255, 0.04); +} + +.sidebar-brand__text h2{ + font-size: 0.96rem; + text-transform: uppercase; +} + +.sidebar-brand__text p{ + color: rgba(215, 225, 235, 0.64); +} + +.p-button{ + min-height: 2.8rem; + border-radius: 10px; + border-width: 1px; + letter-spacing: 0.12em; + transition: transform 0.12s ease, filter 0.12s ease, border-color 0.12s ease, background-color 0.12s ease; +} + +.p-button:not(.p-disabled):hover{ + transform: translateY(-1px); +} + +.p-button.p-button-secondary, .p-button.p-button-help, .p-button.p-button-outlined, .p-button.p-button-text{ + background: var(--surface-1); +} + +body.dark-theme .p-button.p-button-secondary, body.dark-theme .p-button.p-button-help, body.dark-theme .p-button.p-button-outlined, body.dark-theme .p-button.p-button-text{ + background: rgba(255, 255, 255, 0.03); +} + +.p-button.p-button-text{ + background: transparent; +} + +.p-button.table-action-btn{ + width: 2.35rem; + min-width: 2.35rem; + height: 2.35rem; + padding: 0; + border-radius: 10px; +} + + + +.header-number-input{ + max-width: 6rem; +} + +.settings-toggle-grid{ + display: grid; + gap: 0.85rem; + margin-bottom: 1rem; +} + +.settings-toggle{ + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.95rem 1rem; + border: 1px solid var(--border-color); + border-radius: 16px; + background: color-mix(in srgb, var(--surface-1) 86%, transparent); +} + +.settings-toggle strong, .settings-toggle small{ + display: block; +} + +.settings-toggle strong{ + font-family: var(--font-title); + font-size: 0.9rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.settings-toggle small{ + margin-top: 0.3rem; + color: var(--text-soft); + line-height: 1.5; +} + +.settings-toggle--full{ + grid-column: 1 / -1; +} + +.p-inputswitch{ + width: 3.7rem; + height: 1.95rem; +} + +.p-inputswitch .p-inputswitch-slider{ + background: var(--surface-3); + box-shadow: inset 0 0 0 1px var(--border-strong); +} + +.p-inputswitch .p-inputswitch-slider::before{ + width: 1.32rem; + height: 1.32rem; + left: 0.2rem; + margin-top: -0.66rem; + background: #fff; +} + +body.dark-theme .p-inputswitch .p-inputswitch-slider::before{ + background: #dce5ee; +} + +.p-inputswitch.p-inputswitch-checked .p-inputswitch-slider{ + background: linear-gradient(90deg, var(--accent), #d38d5a); +} + +body.dark-theme .p-inputswitch.p-inputswitch-checked .p-inputswitch-slider{ + background: linear-gradient(90deg, #356fae, #4b90d9); +} + + + + + + + + + + + + + + + + + + + + + +@media (max-width: 991px) { + .topbar__right{ + justify-content: space-between; + } + + +} + +/* v7 subtle polish */ +html{ + font-size: 14px; +} + +body{ + font-size: 0.95rem; +} + +body::before{ + opacity: 0.14; +} + +body.dark-theme{ + --surface-0: rgba(28, 38, 49, 0.94); + --surface-1: #1c2631; + --surface-2: #202b36; + --surface-3: #2a3642; + --surface-4: #364353; + --border-color: rgba(160, 183, 206, 0.12); + --border-strong: rgba(160, 183, 206, 0.18); + --text-main: #d6e0e8; + --text-soft: #8ea1b2; + --text-faint: #748696; + --shadow-lg: 0 18px 38px rgba(0, 0, 0, 0.18); + --shadow-md: 0 8px 20px rgba(0, 0, 0, 0.14); +} + +.topbar{ + min-height: 64px; + padding: 0.8rem 1.5rem; + background: rgba(248, 248, 245, 0.74); + backdrop-filter: blur(8px); +} + +body.dark-theme .topbar{ + background: rgba(19, 27, 35, 0.88); +} + +.topbar__caption, .page-header__eyebrow, .sidebar-section__label, .stat-card__label, .hero-spec span, .dashboard-hero__eyebrow, .hero-terminal small, .layout-footer span, .sidebar-footer__title{ + font-size: 0.68rem; + letter-spacing: 0.14em; +} + +.topbar__headline{ + font-size: 0.94rem; + letter-spacing: 0.08em; +} + +.topbar__status{ + padding: 0.45rem 0.7rem; + font-size: 0.7rem; + letter-spacing: 0.12em; + background: color-mix(in srgb, var(--surface-1) 92%, transparent); +} + +.topbar__icon-btn.p-button, .topbar__logout-btn.p-button{ + height: 2.45rem; + min-width: 2.45rem; +} + +.sidebar-brand{ + padding-bottom: 1rem; +} + +.sidebar-nav__item{ + min-height: 40px; + padding: 0.62rem 0.75rem; + border-radius: 10px; +} + +.sidebar-nav__item i{ + font-size: 0.92rem; +} + +.page-header{ + padding: 1.6rem 0 1rem; + margin-bottom: 1.2rem; +} + +.page-header__title{ + font-size: clamp(1.65rem, 3vw, 2.6rem); + letter-spacing: 0.05em; +} + +.page-header__subtitle, .section-card__subtitle{ + font-size: 0.92rem; + line-height: 1.65; +} + +.section-card.p-card, .stat-card.p-card{ + border-radius: 18px; + box-shadow: var(--shadow-md); +} + +.section-card .p-card-content, .stat-card .p-card-content{ + padding: 1rem 1.05rem; +} + +.section-card__header{ + padding-bottom: 0.8rem; + margin-bottom: 0.8rem; +} + +.section-card__title{ + font-size: 0.96rem; + letter-spacing: 0.06em; +} + +.stat-card__value{ + font-size: clamp(1.35rem, 2.2vw, 1.8rem); +} + +.stat-card__hint, .metric-tile span, .table-secondary, .form-field label, .topbar__user-meta small, .settings-toggle small{ + font-size: 0.78rem; +} + +body.dark-theme .dashboard-hero__title{ + font-size: clamp(1.8rem, 4vw, 3.1rem); + letter-spacing: 0.04em; +} + +.metric-tile strong, .settings-toggle strong{ + font-size: 0.88rem; +} + +.metric-tile, .settings-toggle{ + border-radius: 12px; +} + +.p-button{ + min-height: 2.55rem; + padding: 0.66rem 0.9rem; + font-size: 0.74rem; + font-weight: 600; + letter-spacing: 0.1em; + border-radius: 9px; +} + +.p-button:not(.p-disabled):hover{ + transform: none; + filter: brightness(1.04); +} + +.p-button.p-button-secondary, .p-button.p-button-help, .p-button.p-button-outlined, .p-button.p-button-text{ + background: color-mix(in srgb, var(--surface-1) 92%, transparent); +} + +.p-button.table-action-btn{ + width: 2.15rem; + min-width: 2.15rem; + height: 2.15rem; + border-radius: 8px; +} + +.p-inputtext, .p-dropdown, .p-inputtextarea, textarea.p-inputtextarea{ + border-radius: 10px; + padding: 0.72rem 0.82rem; + font-size: 0.92rem; + background: color-mix(in srgb, var(--surface-1) 88%, transparent); +} + +.p-inputswitch{ + width: 3.2rem; + height: 1.7rem; +} + +.p-inputswitch .p-inputswitch-slider::before{ + width: 1.1rem; + height: 1.1rem; + margin-top: -0.55rem; +} + +.p-tag{ + font-size: 0.66rem; +} + +.app-table{ + border-radius: 14px; +} + +.app-table .p-datatable-wrapper{ + border-radius: 14px; +} + +.app-table .p-datatable-thead > tr > th{ + font-size: 0.7rem; + letter-spacing: 0.1em; + padding: 0.8rem 0.9rem; +} + +.app-table .p-datatable-tbody > tr > td{ + padding: 0.82rem 0.9rem; + font-size: 0.9rem; +} + + + + + + + + + + + + + + + + + + + + + + + + + +.form-field{ + gap: 0.4rem; +} + +.form-field label{ + font-size: 0.72rem; + letter-spacing: 0.12em; +} + +@media (max-width: 991px) { + + + +} + + +/* v8 final polish */ +html{ + font-size: 14px; +} + +body{ + font-size: 0.92rem; +} + +.layout-content{ + padding: 0 1.5rem 1.5rem; +} + +.topbar{ + min-height: 68px; + padding: 0.85rem 1.5rem; + background: rgba(255, 255, 255, 0.12); +} + +body.dark-theme .topbar{ + background: rgba(23, 33, 43, 0.74); +} + +.topbar__headline{ + font-size: 1.2rem; + letter-spacing: 0.03em; +} + +.topbar__caption{ + font-size: 0.65rem; +} + +.topbar__user-meta strong{ + font-size: 0.86rem; +} + +.topbar__user-meta small{ + font-size: 0.68rem; +} + +.layout-sidebar{ + padding: 1rem 0.9rem; +} + +.sidebar-brand__logo{ + width: 2.2rem; + height: 2.2rem; + font-size: 0.8rem; +} + +.sidebar-brand__text h2{ + font-size: 0.95rem; +} + +.sidebar-brand__text p, .sidebar-section__label, .sidebar-nav__item span{ + font-size: 0.78rem; +} + +.sidebar-nav__item{ + min-height: 2.5rem; + border-radius: 10px; +} + +.p-button{ + min-height: 2.45rem; + padding: 0.55rem 0.9rem; + border-radius: 10px; + font-size: 0.82rem; + box-shadow: none; +} + +.p-button .p-button-label{ + font-weight: 500; +} + +.p-button.table-action-btn, .topbar__icon-btn.p-button{ + min-height: 2.2rem; + width: 2.2rem; + padding: 0; +} + +.topbar__logout-btn.p-button{ + min-height: 2.2rem; + padding: 0.45rem 0.75rem; +} + + + + + + + + + + + + + + + + + + + + + +.form-field label{ + font-size: 0.72rem; +} + +.p-inputtext, .p-dropdown, .p-multiselect, .p-inputnumber-input, .p-password-input{ + min-height: 2.5rem; + font-size: 0.84rem; +} + +.p-inputswitch{ + transform: scale(0.88); + transform-origin: left center; +} + +.page-header__title{ + font-size: 1.55rem; +} + +.section-card__title, .stat-card__value{ + font-size: 0.95rem; +} + +.p-datatable .p-datatable-thead > tr > th, .p-datatable .p-datatable-tbody > tr > td{ + font-size: 0.79rem; +} + +.status-dot{ + display: none; +} + +/* v9 UI fixes */ +:root{ + --primary-contrast: #ffffff; +} + +body.dark-theme{ + --primary-contrast: #101316; +} + +.p-button{ + color: var(--primary-contrast); +} + +.p-button.p-button-secondary, .p-button.p-button-help, .p-button.p-button-outlined, .p-button.p-button-text{ + color: var(--text-main); +} + +.metric-tile{ + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + gap: 0.45rem; + min-height: 6rem; +} + +.metric-tile span, .metric-tile strong{ + white-space: normal; + word-break: break-word; +} + +.repository-card__actions{ + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.repository-table .p-datatable-tbody > tr > td{ + vertical-align: top; +} + +.table-actions--labels{ + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.p-button.table-action-btn--wide{ + width: auto; + min-width: auto; + padding-inline: 0.8rem; +} + +.preview-dialog .p-dialog-header{ + align-items: center; +} + +.preview-dialog__actions{ + margin-bottom: 0.85rem; + justify-content: flex-end; +} + +.preview-dialog__content{ + max-height: 68vh; + margin: 0; +} + +.change-password-form{ + max-width: none; + width: 100%; +} + +.p-toast{ + width: min(26rem, calc(100vw - 2rem)); +} + +.p-toast .p-toast-message{ + border-radius: 16px; + border: 1px solid var(--border-color); + box-shadow: var(--shadow-md); + overflow: hidden; +} + +.p-toast .p-toast-message-content{ + gap: 0.8rem; +} + +.p-confirm-dialog .p-dialog-content{ + line-height: 1.65; +} + +@media (max-width: 720px) { + .change-password-form{ + grid-template-columns: 1fr; + } + + .table-actions--labels{ + flex-direction: column; + align-items: stretch; + } +} + + +/* --- 2026 UI refresh --- */ +.sidebar-brand__logo img{ + width: 100%; + height: 100%; + object-fit: contain; +} + +.sidebar-brand__logo{ + width: 3.25rem; + height: 3.25rem; + border-radius: 22px; + background: rgba(255, 255, 255, 0.92); + padding: 0.55rem; + display: grid; + place-items: center; + box-shadow: inset 0 0 0 1px rgba(17, 20, 23, 0.08); +} + +.topbar__user{ + padding: 0.35rem 0.65rem 0.35rem 0.35rem; + border: 1px solid var(--border-color); + border-radius: 999px; + background: var(--surface-1); +} + +.topbar__avatar{ + background: linear-gradient(135deg, var(--blue), var(--accent)); + color: #fff; + border: 2px solid rgba(255, 255, 255, 0.16); + box-shadow: 0 10px 24px rgba(75, 144, 217, 0.22); +} + +body.dark-theme .topbar__avatar{ + color: #f8fbff; + border-color: rgba(255, 255, 255, 0.18); +} + +.dashboard-grid--feature{ + align-items: stretch; + margin-top: 1.5rem; +} + +app-section-card.dashboard-operations-card{ + display: block; + margin-bottom: 0.5rem; +} + +.storage-panel{ + display: grid; + grid-template-columns: minmax(220px, 320px) minmax(0, 1fr); + gap: 1.25rem; + align-items: center; +} + +.storage-panel__visual, .storage-panel__stats{ + min-width: 0; +} + +.storage-ring{ + width: 180px; + height: 180px; + border-radius: 50%; + padding: 14px; + margin-bottom: 1rem; +} + +.storage-ring__inner{ + width: 100%; + height: 100%; + border-radius: 50%; + background: var(--surface-1); + display: grid; + place-items: center; + text-align: center; + border: 1px solid var(--border-color); +} + +.storage-ring__inner strong{ + display: block; + font-size: 1.8rem; + line-height: 1; +} + +.storage-ring__inner span{ + color: var(--text-soft); + font-size: 0.78rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.storage-bars{ + display: grid; + gap: 0.9rem; +} + +.storage-bars__meta{ + display: flex; + justify-content: space-between; + gap: 0.8rem; + margin-bottom: 0.35rem; +} + +.storage-bars__track{ + height: 12px; + border-radius: 999px; + background: rgba(128, 145, 164, 0.14); + overflow: hidden; +} + +.storage-bars__track span{ + display: block; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, var(--accent), var(--blue)); +} + +.metric-grid-2--dense{ + gap: 0.95rem; +} + +.metric-tile--feature{ + min-height: 106px; + justify-content: space-between; +} + + +.repository-toolbar, .compare-strip{ + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + gap: 0.9rem; + margin-bottom: 1rem; +} + +.repository-toolbar__search{ + grid-column: span 4; +} + +.repository-toolbar .form-field, .compare-strip__slot{ + grid-column: span 2; +} + +.repository-toolbar__actions, .compare-strip__actions{ + grid-column: span 2; + align-self: end; +} + +.compare-strip{ + padding: 1rem; + border: 1px solid var(--border-color); + border-radius: 22px; + background: linear-gradient(180deg, rgba(75, 144, 217, 0.06), rgba(75, 144, 217, 0.02)); +} + +.compare-strip__swap{ + align-self: end; +} + +.table-actions--stack{ + flex-direction: column; + align-items: stretch; +} + +.github-diff{ + border: 1px solid var(--border-color); + border-radius: 18px; + overflow: hidden; + max-height: 70vh; + overflow-y: auto; +} + +.github-diff__row{ + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); +} + +.github-diff__cell{ + display: grid; + grid-template-columns: 56px minmax(0, 1fr); + min-width: 0; +} + +.github-diff__number{ + padding: 0.65rem 0.45rem; + border-right: 1px solid var(--border-color); + text-align: right; + color: var(--text-faint); + background: rgba(128, 145, 164, 0.08); + font-family: var(--font-title); + font-size: 0.78rem; +} + +.github-diff__cell pre{ + margin: 0; + padding: 0.65rem 0.85rem; + white-space: pre-wrap; + word-break: break-word; + font-family: var(--font-title); + line-height: 1.5; +} + +.github-diff__row[data-type="added"] .github-diff__cell--right, .github-diff__row[data-type="modified"] .github-diff__cell--right{ + background: rgba(79, 181, 147, 0.12); +} + +.github-diff__row[data-type="removed"] .github-diff__cell--left, .github-diff__row[data-type="modified"] .github-diff__cell--left{ + background: rgba(227, 122, 122, 0.12); +} + +.diff-layout__summary{ + display: grid; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto auto; + gap: 1rem; + align-items: center; + margin-bottom: 1rem; +} + +.diff-layout__summary-arrow{ + color: var(--text-faint); +} + +.diff-stats{ + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.diff-stats__pill{ + padding: 0.45rem 0.7rem; + border-radius: 999px; + font-family: var(--font-title); + font-size: 0.78rem; +} + +.diff-stats__pill--added{ + background: rgba(79, 181, 147, 0.14); + color: var(--success); +} + +.diff-stats__pill--removed{ + background: rgba(227, 122, 122, 0.14); + color: var(--danger); +} + +.diff-stats__pill--modified{ + background: rgba(240, 180, 91, 0.14); + color: var(--warning); +} + +.settings-grid--enhanced{ + align-items: start; +} + +.settings-status-grid{ + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 1rem; + margin-bottom: 1.35rem; +} + +.settings-status-card{ + padding: 1rem; + border: 1px solid var(--border-color); + border-radius: 22px; + background: var(--surface-0); + box-shadow: var(--shadow-md); +} + +.settings-status-card__header{ + display: flex; + justify-content: space-between; + gap: 1rem; + margin-bottom: 0.75rem; +} + +.settings-status-card__description{ + font-family: var(--font-title); + margin-bottom: 0.25rem; +} + +.settings-scheduler-stack{ + display: grid; + gap: 1rem; +} + +.scheduler-card{ + padding: 1rem; + border-radius: 22px; + border: 1px solid var(--border-color); + background: linear-gradient(180deg, rgba(75, 144, 217, 0.05), rgba(75, 144, 217, 0.01)); +} + +.scheduler-card__header{ + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; + margin-bottom: 1rem; +} + +.scheduler-card__header small{ + display: block; + margin-top: 0.3rem; +} + +.scheduler-card__grid{ + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.9rem; +} + +.time-picker{ + display: grid; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + align-items: center; + gap: 0.45rem; +} + +.settings-toggle--inline{ + gap: 0.7rem; +} + +.settings-actions--sticky{ + position: sticky; + bottom: 1rem; + justify-content: flex-end; + padding: 0.8rem 1rem; + border-radius: 18px; + background: rgba(23, 33, 43, 0.08); + backdrop-filter: blur(10px); +} + +body.dark-theme .settings-actions--sticky{ + background: rgba(7, 12, 18, 0.45); +} + +.change-password-shell{ + display: grid; + grid-template-columns: minmax(280px, 360px) minmax(0, 1fr); + gap: 1.25rem; +} + +.password-insights{ + display: grid; + gap: 1rem; +} + +.password-strength{ + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; +} + +.password-strength__track{ + height: 12px; + border-radius: 999px; + background: rgba(128, 145, 164, 0.14); + overflow: hidden; +} + +.password-strength__track span{ + display: block; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, var(--danger), var(--warning), var(--success)); +} + +.password-checklist{ + display: grid; + gap: 0.75rem; +} + +.password-checklist__item{ + display: flex; + align-items: center; + gap: 0.7rem; + color: var(--text-soft); +} + +.password-checklist__item.is-ready{ + color: var(--text-main); +} + +.change-password-form--expanded{ + align-items: start; +} + +@media (max-width: 1280px) { + .repository-toolbar__search, .repository-toolbar .form-field, .repository-toolbar__actions, .compare-strip__slot, .compare-strip__actions{ + grid-column: span 6; + } + + .settings-status-grid{ + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .scheduler-card__grid, .storage-panel, .change-password-shell{ + grid-template-columns: 1fr; + } +} + +@media (max-width: 860px) { + .diff-layout__summary{ + grid-template-columns: 1fr; + } + + .github-diff__row{ + grid-template-columns: 1fr; + } + + .repository-toolbar__search, .repository-toolbar .form-field, .repository-toolbar__actions, .compare-strip__slot, .compare-strip__actions, .settings-status-grid{ + grid-column: span 12; + } + + .settings-status-grid{ + grid-template-columns: 1fr; + } +} + +/* Normalize PrimeNG dropdown labels so selected values inherit the field style + instead of rendering like nested inputs. */ +.p-dropdown{ + display: flex; + align-items: center; + min-height: 2.75rem; +} + +.p-dropdown .p-dropdown-label, .p-dropdown .p-dropdown-label.p-inputtext, .p-multiselect .p-multiselect-label{ + width: 100%; + min-height: 0; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; + display: flex; + align-items: center; + font-size: inherit; + font-weight: 500; + line-height: 1.25; + letter-spacing: 0; + text-transform: none; + color: var(--text-main); +} + +.p-dropdown .p-dropdown-label.p-placeholder, .p-multiselect .p-multiselect-label.p-placeholder{ + color: var(--text-soft); + font-weight: 400; +} + +.p-dropdown .p-dropdown-trigger, .p-multiselect .p-multiselect-trigger{ + display: grid; + place-items: center; + align-self: stretch; + color: var(--text-soft); +} + +.p-dropdown-panel .p-dropdown-item, .p-multiselect-panel .p-multiselect-item{ + font-size: 0.84rem; + line-height: 1.35; +} + +/* patch set: settings, dashboard, repository, logs */ +.inline-summary{ + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; + margin-bottom: 1rem; + padding: 0.95rem 1.05rem; + border: 1px solid var(--border-color); + border-radius: 18px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0.18)); +} + +body.dark-theme .inline-summary{ + background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01)); +} + +.inline-summary--soft{ + box-shadow: var(--shadow-md); +} + +.inline-summary--tight{ + margin-bottom: 1.2rem; +} + +.inline-summary__item{ + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.inline-summary__item strong{ + font-family: var(--font-title); + font-size: 1rem; + letter-spacing: 0.04em; +} + +.inline-summary__item span{ + color: var(--text-soft); + font-size: 0.76rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.inline-summary__divider{ + width: 1px; + align-self: stretch; + background: var(--border-color); +} + +.settings-automation-intro{ + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; + margin-bottom: 1rem; + padding: 0.95rem 1rem; + border: 1px dashed var(--border-strong); + border-radius: 18px; + background: rgba(75, 144, 217, 0.04); +} + +.settings-automation-intro strong, .repository-compare__header strong{ + font-family: var(--font-title); + font-size: 0.95rem; + letter-spacing: 0.05em; +} + +.settings-automation-intro p, .repository-compare__header p, .scheduler-card__hint{ + margin: 0.25rem 0 0; + color: var(--text-soft); + line-height: 1.55; +} + +.scheduler-card__hint{ + margin-bottom: 1rem; + font-size: 0.84rem; +} + +.choice-toggle{ + display: inline-grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.35rem; + padding: 0.3rem; + border: 1px solid var(--border-color); + border-radius: 999px; + background: color-mix(in srgb, var(--surface-1) 92%, transparent); +} + +.choice-toggle__btn{ + min-width: 92px; + border: 0; + border-radius: 999px; + padding: 0.52rem 0.8rem; + background: transparent; + color: var(--text-soft); + font-family: var(--font-title); + font-size: 0.72rem; + letter-spacing: 0.12em; + text-transform: uppercase; + cursor: pointer; + transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease; +} + +.choice-toggle__btn.is-active{ + background: var(--primary); + color: #fff; + box-shadow: var(--shadow-md); +} + +.settings-toggle{ + align-items: flex-start; +} + +.operations-center{ + display: grid; + grid-template-columns: minmax(240px, 320px) minmax(0, 1fr); + gap: 1rem; + align-items: stretch; +} + +.operations-center__actions{ + display: grid; + gap: 0.85rem; + padding: 1rem; + border-radius: 20px; + border: 1px solid var(--border-color); + background: linear-gradient(180deg, rgba(75, 144, 217, 0.06), rgba(75, 144, 217, 0.015)); +} + +.operations-center__actions .p-button{ + justify-content: center; +} + +.operations-center__stats{ + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.85rem; +} + +.operations-center__stats .metric-tile small{ + margin-top: 0.35rem; + color: var(--text-soft); + line-height: 1.45; +} + +.repository-compare{ + margin-top: 1rem; + padding: 1rem; + border: 1px solid var(--border-color); + border-radius: 22px; + background: linear-gradient(180deg, rgba(75, 144, 217, 0.06), rgba(75, 144, 217, 0.02)); +} + +.repository-compare__header{ + display: flex; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; + align-items: flex-start; +} + +.repository-compare__status{ + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 0.5rem; +} + +.repository-compare__grid{ + display: grid; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto; + gap: 0.9rem; + align-items: end; +} + +.repository-compare__slot{ + min-width: 0; +} + +.repository-compare__actions{ + display: grid; + gap: 0.65rem; +} + +.repository-table .p-datatable-tbody > tr[data-compare-role="left"] > td{ + background: rgba(240, 180, 91, 0.07); +} + +.repository-table .p-datatable-tbody > tr[data-compare-role="right"] > td{ + background: rgba(79, 181, 147, 0.08); +} + +.repository-table .p-datatable-tbody > tr[data-compare-role="left"]:hover > td, .repository-table .p-datatable-tbody > tr[data-compare-role="right"]:hover > td{ + filter: brightness(1.01); +} + +.change-password-form .p-inputtext{ + width: 100%; +} + +@media (max-width: 1280px) { + .operations-center, .operations-center__stats, .repository-compare__grid{ + grid-template-columns: 1fr; + } + + .repository-compare__status{ + justify-content: flex-start; + } +} + +@media (max-width: 860px) { + .settings-automation-intro, .repository-compare__header{ + flex-direction: column; + } + + .choice-toggle{ + width: 100%; + } + + .choice-toggle__btn{ + min-width: 0; + } + + .inline-summary__divider{ + display: none; + } +} + + +.app-auth-view{ + min-height: 100vh; + display: flex; + flex-direction: column; +} + + + +.layout-footer a{ + color: var(--accent); +} + +.layout-footer__status{ + display: inline-flex; + align-items: center; + gap: 0.45rem; +} + +.layout-footer__status::before{ + content: ""; + width: 0.6rem; + height: 0.6rem; + border-radius: 999px; + background: var(--text-faint); + box-shadow: 0 0 0 4px rgba(128, 145, 164, 0.12); +} + +.layout-footer__status--online::before{ + background: var(--success); + box-shadow: 0 0 0 4px rgba(79, 181, 147, 0.16); +} + +.layout-footer__status--offline::before{ + background: var(--danger); + box-shadow: 0 0 0 4px rgba(195, 70, 70, 0.14); +} + +.layout-footer__status--checking::before{ + background: var(--warning); + box-shadow: 0 0 0 4px rgba(240, 180, 91, 0.14); +} + +.layout-footer--auth{ + margin-top: auto; + justify-content: center; +} + +.api-connection-banner{ + position: fixed; + right: 1.5rem; + bottom: 1.5rem; + z-index: 120; + display: flex; + align-items: center; + gap: 0.9rem; + width: min(28rem, calc(100vw - 2rem)); + padding: 0.95rem 1rem; + border-radius: 18px; + border: 1px solid rgba(195, 70, 70, 0.22); + background: rgba(255, 248, 248, 0.96); + box-shadow: var(--shadow-lg); +} + +body.dark-theme .api-connection-banner{ + background: rgba(29, 39, 51, 0.96); +} + +.api-connection-banner__content{ + display: grid; + gap: 0.25rem; +} + +.api-connection-banner__content strong{ + font-size: 0.84rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--danger); +} + +.api-connection-banner__content span{ + color: var(--text-soft); + font-size: 0.9rem; + line-height: 1.5; +} + +.api-connection-banner__action{ + flex-shrink: 0; + border: 1px solid rgba(195, 70, 70, 0.2); + border-radius: 999px; + background: transparent; + color: var(--danger); + padding: 0.65rem 0.95rem; + font: inherit; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + cursor: pointer; +} + +.api-connection-banner__action:hover{ + background: rgba(195, 70, 70, 0.08); +} + + +/* --- router detail adjustments --- */ +.router-detail-grid{ + align-items: start; +} + +.router-detail-grid--stack{ + grid-template-columns: 1fr; +} + +.router-status-panel{ + display: grid; + gap: 0.9rem; +} + +.router-status-error{ + display: grid; + gap: 0.35rem; + padding: 0.85rem 1rem; + border-radius: 16px; + border: 1px solid rgba(217, 75, 91, 0.24); + background: rgba(217, 75, 91, 0.08); +} + +.router-status-note{ + color: var(--text-soft); +} + +.table-actions--tight{ + flex-wrap: nowrap; + gap: 0.4rem; + overflow-x: auto; + padding-bottom: 0.15rem; +} + +.p-button.table-action-btn--compact{ + min-height: 2rem; + padding-inline: 0.6rem; + font-size: 0.78rem; +} + +/* --- language selector + settings layout --- */ +.topbar__lang-picker{ + position: relative; +} + +.topbar__lang-select{ + min-height: 2.5rem; + padding: 0.55rem 2.15rem 0.55rem 0.85rem; + border-radius: 999px; + border: 1px solid var(--border-color); + background: color-mix(in srgb, var(--surface-1) 92%, transparent); + color: var(--text-main); + font: inherit; + appearance: none; + cursor: pointer; + box-shadow: var(--shadow-sm); + color-scheme: light; +} + +.topbar__lang-select{ + min-width: 132px; +} + +.topbar__lang-picker::after{ + content: "▾"; + position: absolute; + right: 0.8rem; + top: 50%; + transform: translateY(-50%); + pointer-events: none; + color: var(--text-soft); +} + +.topbar__lang-select option{ + background: var(--surface-1); + color: var(--text-main); +} + +body.dark-theme .topbar__lang-select{ + background: rgba(15, 21, 29, 0.92); + color: var(--text-main); + border-color: var(--border-color); + color-scheme: dark; +} + +body.dark-theme .topbar__lang-select option{ + background: #1d2733; + color: #dae4ec; +} + +.settings-page-shell{ + display: grid; + gap: 1rem; +} + +.settings-page-columns{ + display: grid; + grid-template-columns: minmax(0, 1.65fr) minmax(320px, 0.95fr); + gap: 1rem; + align-items: start; +} + +.settings-page-main, .settings-page-side{ + display: grid; + gap: 1rem; +} + +.settings-collapse{ + border: 1px solid var(--border-color); + border-radius: 24px; + background: linear-gradient(180deg, var(--surface-1) 0%, var(--surface-0) 100%); + box-shadow: var(--shadow-md); + overflow: hidden; +} + +.settings-collapse > summary{ + position: relative; + list-style: none; + cursor: pointer; + padding: 1rem 3rem 1rem 1.15rem; + display: grid; + gap: 0.2rem; +} + +.settings-collapse > summary::-webkit-details-marker{ + display: none; +} + +.settings-collapse > summary span{ + font-family: var(--font-title); + font-size: 1rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.settings-collapse > summary small{ + color: var(--text-soft); +} + +.settings-collapse > summary::after{ + content: ""; + font-family: "primeicons"; + position: absolute; + right: 1.1rem; + top: 50%; + transform: translateY(-50%) rotate(0deg); + color: var(--text-soft); + transition: transform 0.2s ease; +} + +.settings-collapse[open] > summary::after{ + transform: translateY(-50%) rotate(180deg); +} + +.settings-collapse__body{ + padding: 0.85rem 1.15rem 1.15rem; + border-top: 1px solid var(--border-color); +} + +.settings-collapse__body > :first-child, .section-card .p-card-content > :first-child{ + margin-top: 0; +} + +.settings-collapse--sticky{ + position: sticky; + top: 1rem; +} + +.settings-scheduler-stack{ + margin-top: 1rem; +} + +.scheduler-card--subtle{ + background: linear-gradient(180deg, rgba(79, 181, 147, 0.08), rgba(79, 181, 147, 0.02)); +} + +.scheduler-card__grid--compact{ + grid-template-columns: minmax(0, 220px); +} + +.settings-ssh-panel{ + display: grid; + gap: 1rem; +} + +.settings-ssh-panel__header{ + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; +} + +.settings-ssh-panel__header p, .settings-ssh-lock p, .settings-ssh-note{ + color: var(--text-soft); +} + +.settings-ssh-lock{ + display: grid; + gap: 0.85rem; + padding: 1rem; + border-radius: 18px; + border: 1px dashed var(--border-color); + background: rgba(75, 144, 217, 0.05); +} + +.settings-ssh-actions{ + justify-content: flex-end; +} + + + +app-page-header, app-section-card, app-stat-card, app-sidebar, app-topbar{ + display: block; +} + +app-page-header{ + margin-bottom: 1rem; +} + +.section-card__header + *, .page-header__actions + *{ + margin-top: 0.25rem; +} + +.p-tag, .p-tag .p-tag-value{ + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +@media (max-width: 1120px) { + .settings-page-columns{ + grid-template-columns: 1fr; + } + + .settings-collapse--sticky{ + position: static; + } +} + +@media (max-width: 860px) { + .table-actions--tight{ + flex-wrap: wrap; + overflow-x: visible; + } + + .settings-collapse__body{ + padding-inline: 1rem; + } +} + +.router-detail-grid--inspection{ + align-items: start; +} + +.router-detail-inspection-stack{ + display: grid; + gap: 1rem; +} + +.router-detail-grid--stack{ + margin-top: 1rem; +} + +.router-modal-summary{ + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; + padding: 1rem; + border: 1px dashed var(--border-color); + border-radius: 18px; + background: linear-gradient(180deg, rgba(75, 144, 217, 0.05), rgba(75, 144, 217, 0.015)); +} + +.router-modal-summary strong, .diff-pick-card__meta strong, .activity-feed__header strong{ + display: block; + margin: 0; +} + +.router-modal-summary small, .diff-pick-card__meta small{ + display: block; + margin-top: 0.35rem; + color: var(--text-soft); +} + +.repository-table, .dashboard-grid > app-section-card, .router-detail-inspection-stack > app-section-card{ + width: 100%; +} + +.activity-feed{ + display: grid; + gap: 0.55rem; +} + +.activity-feed__item{ + display: grid; + grid-template-columns: 28px minmax(0, 1fr); + gap: 0.75rem; + align-items: start; + padding: 0.75rem 0; + border-bottom: 1px solid var(--border-color); +} + +.activity-feed__item:last-child{ + border-bottom: 0; +} + +.activity-feed__dot{ + width: 28px; + height: 28px; + border-radius: 999px; + display: grid; + place-items: center; + color: var(--text-soft); + background: rgba(75, 144, 217, 0.08); + border: 1px solid rgba(75, 144, 217, 0.12); + font-size: 0.8rem; +} + +.activity-feed__item[data-tone="danger"] .activity-feed__dot{ + color: var(--danger); + background: rgba(227, 122, 122, 0.08); + border-color: rgba(227, 122, 122, 0.18); +} + +.activity-feed__item[data-tone="warning"] .activity-feed__dot{ + color: var(--warning); + background: rgba(240, 180, 91, 0.08); + border-color: rgba(240, 180, 91, 0.18); +} + +.activity-feed__item[data-tone="success"] .activity-feed__dot{ + color: var(--success); + background: rgba(79, 181, 147, 0.08); + border-color: rgba(79, 181, 147, 0.18); +} + +.activity-feed__body{ + min-width: 0; +} + +.activity-feed__header{ + display: flex; + justify-content: space-between; + gap: 0.75rem; + align-items: center; + margin-bottom: 0.2rem; +} + +.activity-feed__header small{ + color: var(--text-faint); + white-space: nowrap; +} + +.activity-feed__message{ + color: var(--text-soft); + line-height: 1.45; + font-size: 0.88rem; +} + +.activity-feed__footer{ + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; + padding-top: 0.4rem; +} + +.diff-workspace{ + display: grid; + gap: 1rem; +} + +.diff-workspace__toolbar{ + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: end; + flex-wrap: wrap; +} + +.diff-workspace__router{ + min-width: min(320px, 100%); +} + +.diff-workspace__actions{ + display: flex; + gap: 0.7rem; + flex-wrap: wrap; +} + +.diff-workspace__pair{ + display: grid; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + gap: 1rem; + align-items: center; +} + +.diff-workspace__swap.p-button{ + align-self: center; +} + +.diff-pick-card{ + display: grid; + gap: 0.9rem; + padding: 1rem; + border-radius: 22px; + border: 1px solid var(--border-color); + background: linear-gradient(180deg, var(--surface-1), rgba(255,255,255,0.02)); +} + +.diff-pick-card.is-selected{ + border-color: rgba(75, 144, 217, 0.3); + box-shadow: var(--shadow-md); +} + +.diff-pick-card__header{ + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; +} + +.diff-pick-card__header strong{ + font-family: var(--font-title); + letter-spacing: 0.06em; +} + +.diff-pick-card__meta{ + padding-top: 0.3rem; + border-top: 1px solid var(--border-color); +} + +@media (max-width: 980px) { + .diff-workspace__pair, .router-detail-grid--inspection{ + grid-template-columns: 1fr; + } + + .diff-workspace__swap.p-button{ + justify-self: stretch; + } +} + +@media (max-width: 720px) { + .activity-feed__header, .router-modal-summary{ + flex-direction: column; + align-items: flex-start; + } +} + + +/* --- patch set: compact repository ux and spacing refinements --- */ +.repository-table-section, .diff-configs-table-section{ + display: block; + margin-top: 1rem; +} + +.repository-table.app-table{ + border-radius: 16px; +} + +.repository-table.app-table .p-datatable-thead > tr > th{ + padding: 0.72rem 0.8rem; + font-size: 0.68rem; + letter-spacing: 0.1em; +} + +.repository-table.app-table .p-datatable-tbody > tr > td{ + padding: 0.68rem 0.8rem; +} + +.repository-table .table-primary{ + font-size: 0.88rem; + line-height: 1.35; +} + +.repository-table .table-secondary{ + margin-top: 0.18rem; + font-size: 0.74rem; + line-height: 1.35; +} + +.repository-table .p-tag{ + font-size: 0.68rem; + padding: 0.22rem 0.48rem; +} + +.repository-table .p-checkbox{ + transform: scale(0.92); + transform-origin: center; +} + +.repository-table .p-datatable-tbody > tr > td:first-child, .repository-table .p-datatable-thead > tr > th:first-child{ + width: 2.6rem; + padding-inline: 0.55rem; +} + +.repository-table .table-actions--stack{ + gap: 0.38rem; +} + +.repository-table .p-button.table-action-btn, .repository-table .p-button.table-action-btn--wide, .repository-table .p-button.table-action-btn--compact{ + min-height: 1.9rem; + padding: 0.45rem 0.62rem; + font-size: 0.7rem; + letter-spacing: 0.09em; +} + +.repository-table .p-button .p-button-label{ + white-space: nowrap; +} + +.repository-table .p-button .p-button-icon{ + font-size: 0.78rem; +} + +.repository-table .p-paginator{ + padding: 0.4rem 0.65rem; +} + +.repository-table .p-paginator .p-paginator-pages .p-paginator-page, .repository-table .p-paginator .p-paginator-next, .repository-table .p-paginator .p-paginator-prev, .repository-table .p-paginator .p-dropdown{ + min-width: 2rem; + height: 2rem; +} + +.repository-compare{ + margin-top: 1.15rem; +} + +.repository-compare__grid{ + margin-top: 0.2rem; +} + +.diff-workspace{ + gap: 1.15rem; +} + +.diff-workspace__pair{ + margin-top: 0.15rem; +} + +.preview-dialog .p-dialog-header{ + padding-bottom: 0.9rem; +} + +.preview-dialog .p-dialog-content{ + padding-top: 1rem; +} + +@media (max-width: 980px) { + .repository-table-section, .diff-configs-table-section{ + margin-top: 0.85rem; + } + + .repository-table.app-table .p-datatable-thead > tr > th, .repository-table.app-table .p-datatable-tbody > tr > td{ + padding-inline: 0.72rem; + } +} + +/* --- 2026 patch: dashboard storage, confirm dialog, interface prefs, switches --- */ +.storage-panel--enhanced{ + gap: 1.15rem; +} + +.storage-browser__switch.is-active{ + color: var(--text-main); + border-color: color-mix(in srgb, var(--accent) 36%, var(--border-color)); + box-shadow: 0 10px 24px color-mix(in srgb, var(--accent) 12%, transparent); +} + +.storage-browser__switch.is-active{ + background: color-mix(in srgb, var(--surface-1) 96%, transparent); +} + +.storage-browser__canvas, .storage-chart{ + display: grid; + gap: 0.85rem; +} + +.storage-chart-row{ + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(140px, 1.25fr) auto; + align-items: center; + gap: 0.85rem; +} + +.storage-chart-row__meta{ + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 1rem; +} + +.storage-chart-row__meta span, .storage-chart-row small, .storage-column small{ + color: var(--text-soft); + font-size: 0.72rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.storage-chart-row__meta strong, .storage-column strong{ + font-size: 0.92rem; +} + +.storage-bars__track span, .storage-chart-row__track span, .storage-stackbar__segment, .storage-column__track span{ + display: block; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, var(--accent), var(--blue)); +} + +.storage-bars__track span[data-tone="success"], .storage-chart-row__track span[data-tone="success"], .storage-stackbar__segment[data-tone="success"]{ + background: linear-gradient(90deg, var(--accent-2), var(--success)); +} + +.storage-bars__track span[data-tone="warning"], .storage-chart-row__track span[data-tone="warning"], .storage-stackbar__segment[data-tone="warning"]{ + background: linear-gradient(90deg, var(--warning), color-mix(in srgb, var(--warning) 44%, var(--danger))); +} + +.storage-bars__track span[data-tone="info"], .storage-chart-row__track span[data-tone="info"], .storage-stackbar__segment[data-tone="info"]{ + background: linear-gradient(90deg, color-mix(in srgb, var(--blue) 78%, #fff 0%), var(--blue)); +} + +.storage-stackbar{ + display: flex; + min-height: 16px; + border-radius: 999px; + overflow: hidden; + background: rgba(128, 145, 164, 0.14); +} + +.storage-columns{ + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + gap: 0.75rem; + align-items: end; +} + +.storage-column{ + display: grid; + gap: 0.55rem; + justify-items: center; +} + +.storage-column__track{ + height: 150px; + width: 100%; + padding: 0 0.35rem; + display: flex; + align-items: flex-end; + border-radius: 18px; + border: 1px solid var(--border-color); + background: linear-gradient(180deg, color-mix(in srgb, var(--surface-1) 92%, transparent), transparent); +} + +.storage-column__track span{ + width: 100%; + min-height: 6px; + border-radius: 14px 14px 8px 8px; + background: linear-gradient(180deg, var(--accent), var(--blue)); +} + +.storage-chart-empty{ + padding: 1rem; + border-radius: 18px; + border: 1px dashed var(--border-strong); + color: var(--text-soft); + text-align: center; +} + +.storage-snapshot-grid{ + gap: 0.8rem; +} + +.storage-snapshot-card{ + min-height: 96px; + border: 1px solid var(--border-color); + background: linear-gradient(180deg, color-mix(in srgb, var(--surface-1) 94%, transparent), color-mix(in srgb, var(--surface-0) 94%, transparent)); +} + +.layout-footer__author{ + display: inline-flex; + align-items: center; + gap: 0.55rem; + padding: 0.45rem 0.85rem; + border-radius: 999px; + border: 1px solid var(--border-color); + background: color-mix(in srgb, var(--surface-1) 90%, transparent); +} + +.layout-footer__author-label{ + color: var(--text-soft); +} + +.layout-footer__author strong{ + font-size: 0.82rem; + text-transform: none; + letter-spacing: 0.04em; +} + +.layout-footer__author a{ + font-size: 0.78rem; + font-weight: 700; +} + +@media (max-width: 1180px) { + .storage-panel{ + grid-template-columns: 1fr; + } +} + +@media (max-width: 860px) { + .storage-hero{ + grid-template-columns: 1fr; + } + + .storage-ring{ + margin-inline: auto; + } + + .storage-browser__switch{ + flex: 1 1 calc(50% - 0.55rem); + } +} + +@media (max-width: 720px) { + .storage-chart-row{ + grid-template-columns: 1fr; + } + + .storage-chart-row__meta{ + flex-direction: column; + gap: 0.2rem; + } + + .storage-column__track{ + height: 120px; + } + + .layout-footer__author{ + width: 100%; + justify-content: center; + } +} + +.settings-interface-intro, .settings-automation-intro{ + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; + padding: 0.95rem 1rem; + border: 1px dashed var(--border-strong); + border-radius: 18px; + background: rgba(75, 144, 217, 0.04); +} + +.settings-interface-intro strong, .settings-automation-intro strong, .repository-compare__header strong{ + font-family: var(--font-title); + font-size: 0.95rem; + letter-spacing: 0.05em; +} + +.settings-interface-intro p, .settings-automation-intro p, .repository-compare__header p, .scheduler-card__hint{ + margin: 0.25rem 0 0; + color: var(--text-soft); + line-height: 1.55; +} + +.choice-toggle{ + overflow: hidden; +} + +.choice-toggle__btn{ + font-weight: 700; +} + +body.dark-theme .choice-toggle{ + background: rgba(8, 14, 22, 0.52); + border-color: rgba(146, 170, 194, 0.24); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); +} + +body.dark-theme .choice-toggle__btn{ + color: #b8c8d8; +} + +body.dark-theme .choice-toggle__btn:hover{ + background: rgba(255, 255, 255, 0.06); + color: #edf3f8; +} + +body.dark-theme .choice-toggle__btn.is-active{ + background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 82%, #fff 0%), color-mix(in srgb, var(--blue) 85%, #fff 0%)); + color: #0f1720; +} + +.p-confirm-dialog{ + border-radius: 24px; + overflow: hidden; + border: 1px solid var(--border-color); + box-shadow: var(--shadow-lg); +} + +.p-confirm-dialog .p-dialog-header{ + padding: 1rem 1.15rem 0.75rem; + background: linear-gradient(180deg, color-mix(in srgb, var(--surface-1) 98%, transparent), color-mix(in srgb, var(--surface-0) 98%, transparent)); + border-bottom: 1px solid var(--border-color); +} + +.p-confirm-dialog .p-dialog-title{ + font-family: var(--font-title); + letter-spacing: 0.08em; +} + +.p-confirm-dialog .p-dialog-content{ + padding: 1rem 1.15rem 0.5rem; + line-height: 1.65; + background: color-mix(in srgb, var(--surface-0) 98%, transparent); +} + +.p-confirm-dialog .p-confirm-dialog-icon{ + width: 2.6rem; + height: 2.6rem; + display: inline-grid; + place-items: center; + border-radius: 16px; + background: color-mix(in srgb, var(--warning) 14%, transparent); + color: var(--warning); + margin-right: 0.85rem; +} + +.p-confirm-dialog .p-dialog-footer{ + display: flex; + justify-content: flex-end; + gap: 0.65rem; + padding: 0.9rem 1.15rem 1rem; + background: color-mix(in srgb, var(--surface-1) 98%, transparent); + border-top: 1px solid var(--border-color); +} + +body.dark-theme .p-confirm-dialog .p-confirm-dialog-icon{ + background: rgba(240, 180, 91, 0.14); +} + +@media (max-width: 980px) { + .storage-hero{ + grid-template-columns: 1fr; + justify-items: center; + text-align: center; + } + + .storage-hero__summary{ + width: 100%; + } +} + + +/* --- 2026 patch: dashboard width, auth layout, settings actions --- */ +.dashboard-stack{ + margin-top: 1.5rem; +} + +.dashboard-stack > app-section-card{ + display: block; +} + +.settings-actions, .settings-actions--sticky{ + justify-content: center; +} + + + + + + + + + +@media (max-width: 991px) { + +} + +/* patch set: dashboard capacity and switchos beta */ +.storage-browser__header{ + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: start; +} + +.storage-browser__copy, .storage-browser__switcher, .storage-chart-row__meta{ + min-width: 0; +} + +.storage-browser__switcher{ + justify-content: flex-end; +} + +.storage-chart-row__meta{ + flex-wrap: wrap; +} + +.storage-chart-row__meta strong{ + white-space: nowrap; + flex-shrink: 0; +} + +.beta-banner{ + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + padding: 1rem 1.05rem; + border-radius: 16px; + border: 1px solid color-mix(in srgb, var(--warning) 36%, var(--border-color)); + background: linear-gradient(180deg, color-mix(in srgb, var(--warning) 10%, transparent), color-mix(in srgb, var(--surface-1) 94%, transparent)); +} + +.beta-banner p{ + margin: 0.45rem 0 0; + color: var(--text-soft); + line-height: 1.6; +} + +.swos-beta-grid{ + align-items: start; +} + +.swos-beta-actions{ + grid-column: 1 / -1; + justify-content: flex-start; + flex-wrap: wrap; +} + +.swos-beta-result{ + display: grid; + gap: 0.85rem; +} + +.swos-beta-result__item{ + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.9rem 1rem; + border-radius: 14px; + border: 1px solid var(--border-color); + background: color-mix(in srgb, var(--surface-1) 90%, transparent); +} + +.swos-beta-result__item span{ + color: var(--text-soft); + font-size: 0.78rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.swos-beta-result__item strong{ + text-align: right; +} + +.swos-beta-note, .beta-error{ + padding: 0.9rem 1rem; + border-radius: 14px; + line-height: 1.6; +} + +.swos-beta-note{ + border: 1px solid var(--border-color); + color: var(--text-soft); + background: color-mix(in srgb, var(--surface-1) 88%, transparent); +} + +.beta-error{ + border: 1px solid color-mix(in srgb, var(--danger) 38%, var(--border-color)); + color: color-mix(in srgb, var(--danger) 82%, var(--text-main)); + background: color-mix(in srgb, var(--danger) 8%, transparent); +} + +@media (max-width: 1024px) { + .storage-browser__header{ + grid-template-columns: 1fr; + } + + .storage-browser__switcher{ + width: 100%; + justify-content: flex-start; + } +} + +@media (max-width: 720px) { + .beta-banner, .swos-beta-result__item{ + flex-direction: column; + align-items: flex-start; + } + + .swos-beta-result__item strong{ + text-align: left; + } +} + +/* 2026-04 layout fixes: auth centering and storage header spacing */ + + + + + + + + + + +.storage-browser__header{ + grid-template-columns: minmax(0, 1fr); + gap: 1rem; +} + +.storage-browser__copy{ + display: grid; + gap: 0.35rem; +} + +.storage-browser__copy strong{ + display: block; +} + +.storage-browser__switcher{ + width: 100%; + justify-content: flex-start; +} + +@media (max-width: 991px) { + +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..fa026e8 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..44011b8 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "strictTemplates": true + } +} \ No newline at end of file diff --git a/make_zip.py b/make_zip.py new file mode 100644 index 0000000..e133aad --- /dev/null +++ b/make_zip.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +import os +import sys +import zipfile +import subprocess +from pathlib import Path + + +def run_git_command(args, repo_path: Path) -> bytes: + result = subprocess.run( + ["git", *args], + cwd=repo_path, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + ) + return result.stdout + + +def get_files_to_archive(repo_path: Path) -> list[str]: + output = run_git_command( + ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], + repo_path, + ) + files = output.decode("utf-8", errors="surrogateescape").split("\0") + return [f for f in files if f] + + +def make_zip(repo_path: Path, output_zip: Path) -> None: + files = get_files_to_archive(repo_path) + + output_zip = output_zip.resolve() + if output_zip.exists(): + output_zip.unlink() + + with zipfile.ZipFile(output_zip, "w", compression=zipfile.ZIP_DEFLATED) as zf: + for rel_path in files: + abs_path = repo_path / rel_path + + if not abs_path.exists(): + continue + + if abs_path.resolve() == output_zip: + continue + + zf.write(abs_path, arcname=rel_path) + + print(f"Utworzono archiwum: {output_zip}") + print(f"Dodano plików: {len(files)}") + + +def main(): + repo_path = Path.cwd() + + if len(sys.argv) > 1: + output_zip = Path(sys.argv[1]) + else: + output_zip = repo_path / f"{repo_path.name}.zip" + + try: + run_git_command(["rev-parse", "--show-toplevel"], repo_path) + except subprocess.CalledProcessError: + print("Błąd: ten katalog nie jest repozytorium Git.", file=sys.stderr) + sys.exit(1) + + make_zip(repo_path, output_zip) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/patch_routeros.py b/patch_routeros.py new file mode 100644 index 0000000..4572e45 --- /dev/null +++ b/patch_routeros.py @@ -0,0 +1,1207 @@ +from pathlib import Path +import json + +root = Path('/mnt/data/work_routeros3/frontend/src') + +(root / 'app/features/dashboard/dashboard-page.component.html').write_text(''' + +
+ + + + +
+ + +
+
+ + +
+ +
+
+ {{ 'dashboard.latestSnapshot' | translate }} + {{ latestBackupLabel }} + {{ latestBackupHint }} +
+
+ {{ 'dashboard.coverageLabel' | translate }} + {{ coveragePercent }}% + {{ 'dashboard.coverageHint' | translate }} +
+
+ {{ 'dashboard.weeklyActivityLabel' | translate }} + {{ backupsLast7Days }} + {{ 'dashboard.weeklyActivityHint' | translate }} +
+
+ {{ 'dashboard.busiestRouterLabel' | translate }} + {{ busiestRouterLabel }} + {{ busiestRouterHint }} +
+
+
+
+ +
+ +
+
+
+
+ +
+
{{ storageViewDescriptionKey | translate }}
+
+ +
+
+
+
+
+ {{ formatPercent(usedPercent) }} + {{ 'dashboard.diskUsage' | translate }} +
+
+ +
+
+ {{ item.label | translate }} + {{ item.value }} +
+
+
+ +
+
+ {{ 'dashboard.diskUsed' | translate }} + {{ formatBytes(storageUsedBytes) }} + {{ 'dashboard.storageInsightUsage' | translate }} +
+ +
+
+
+ {{ 'dashboard.folderUsage' | translate }} + {{ formatPercent(repositorySharePercent) }} +
+
+
+
+
+ {{ 'dashboard.freeSpace' | translate }} + {{ formatPercent(freePercent) }} +
+
+
+
+ +
+
+ {{ 'dashboard.exportShareLabel' | translate }} + {{ exportsSharePercent }}% +
+
+ {{ 'dashboard.activityTodayLabel' | translate }} + {{ activityToday }} +
+
+
+
+ +
+
+
+
+
+ {{ row.label | translate }} + {{ row.value }} +
+
+ +
+ {{ formatPercent(row.percent) }} +
+
+
+ +
+
+ {{ 'dashboard.exportShareLabel' | translate }} + {{ exportsSharePercent }}% +
+
+ {{ 'dashboard.binaryCard' | translate }} + {{ binarySharePercent }}% +
+
+ {{ 'dashboard.avgBackupsPerRouter' | translate }} + {{ averageBackupsPerRouter }} +
+
+
+ +
+
+
+
+ {{ 'dashboard.storageViewActivity' | translate }} + {{ 'dashboard.storageActivityHint' | translate }} +
+ {{ backupsLast7Days }} +
+
+
+
+ +
+ {{ bar.value }} + {{ bar.label }} +
+
+
+ +
+
+ {{ 'dashboard.latestSnapshot' | translate }} + {{ latestBackupLabel }} +
+
+ {{ 'dashboard.busiestRouterLabel' | translate }} + {{ busiestRouterLabel }} +
+
+ {{ 'dashboard.activityTodayLabel' | translate }} + {{ activityToday }} +
+
+
+
+
+ +
+
+
+ {{ 'dashboard.totalDisk' | translate }} + {{ formatBytes(data.storage.total) }} +
+
+ {{ 'dashboard.diskUsed' | translate }} + {{ formatBytes(storageUsedBytes) }} +
+
+ {{ 'dashboard.freeSpace' | translate }} + {{ formatBytes(data.storage.free) }} +
+
+ {{ 'dashboard.folderUsage' | translate }} + {{ formatBytes(data.storage.folder_used) }} +
+
+ {{ 'dashboard.avgBackupsPerRouter' | translate }} + {{ averageBackupsPerRouter }} +
+
+ {{ 'dashboard.activityTodayLabel' | translate }} + {{ activityToday }} +
+
+
+
+
+ + +
+
+
+
+
+ {{ activityLabel(log.message) }} + {{ log.timestamp | date: 'dd.MM.yyyy HH:mm:ss' }} +
+
{{ log.message }}
+
+
+ + +
+ + +
+ +

{{ 'dashboard.noActivity' | translate }}

+
+
+
+
+''') + +(root / 'app/features/dashboard/dashboard-page.component.ts').write_text('''import { CommonModule } from '@angular/common'; +import { Component, OnInit, inject } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { ButtonModule } from 'primeng/button'; +import { forkJoin } from 'rxjs'; + +import { ApiService } from '../../core/services/api.service'; +import { LanguageService } from '../../core/services/language.service'; +import { UiService } from '../../core/services/ui.service'; +import { PageHeaderComponent } from '../../shared/ui/page-header.component'; +import { SectionCardComponent } from '../../shared/ui/section-card.component'; +import { StatCardComponent } from '../../shared/ui/stat-card.component'; + +interface DashboardData { + routers_count: number; + export_count: number; + binary_count: number; + total_backups: number; + recent_logs: { timestamp: string; message: string }[]; + storage: { total: number; used: number; free: number; folder_used: number; usage_percent: number }; +} + +interface BackupInventoryItem { + id: number; + router_id: number; + router_name?: string; + backup_type: 'export' | 'binary'; + created_at: string; + file_size?: number | null; +} + +interface RouterInventoryItem { + id: number; + name: string; +} + +type StorageView = 'overview' | 'composition' | 'activity'; + +@Component({ + standalone: true, + imports: [CommonModule, RouterLink, TranslateModule, ButtonModule, PageHeaderComponent, SectionCardComponent, StatCardComponent], + templateUrl: './dashboard-page.component.html' +}) +export class DashboardPageComponent implements OnInit { + private readonly api = inject(ApiService); + private readonly ui = inject(UiService); + private readonly language = inject(LanguageService); + + data?: DashboardData; + backups: BackupInventoryItem[] = []; + routers: RouterInventoryItem[] = []; + exporting = false; + runningBinary = false; + readonly activityPageSize = 6; + activityPage = 0; + storageView: StorageView = 'overview'; + readonly storageViewOptions: { value: StorageView; label: string; icon: string }[] = [ + { value: 'overview', label: 'dashboard.storageViewOverview', icon: 'pi pi-chart-pie' }, + { value: 'composition', label: 'dashboard.storageViewComposition', icon: 'pi pi-sliders-h' }, + { value: 'activity', label: 'dashboard.storageViewActivity', icon: 'pi pi-chart-bar' } + ]; + + ngOnInit() { + this.load(); + } + + load() { + forkJoin({ + dashboard: this.api.http.get(`${this.api.baseUrl}/dashboard`), + backups: this.api.http.get(`${this.api.baseUrl}/backups`), + routers: this.api.http.get(`${this.api.baseUrl}/routers`) + }).subscribe(({ dashboard, backups, routers }) => { + this.data = dashboard; + this.backups = backups; + this.routers = routers; + this.activityPage = 0; + }); + } + + exportAll() { + if (this.exporting) { + return; + } + this.exporting = true; + this.api.http.post(`${this.api.baseUrl}/backups/routers/export-all`, {}).subscribe({ + next: (result) => { + this.ui.success('toast.exportedRouters', { count: result.filter((item) => item.status === 'ok').length }); + this.load(); + }, + complete: () => { + this.exporting = false; + } + }); + } + + binaryAll() { + if (this.runningBinary) { + return; + } + this.runningBinary = true; + this.api.http.post(`${this.api.baseUrl}/backups/routers/binary-all`, {}).subscribe({ + next: (result) => { + this.ui.success('toast.binaryCompletedRouters', { count: result.filter((item) => item.status === 'ok').length }); + this.load(); + }, + complete: () => { + this.runningBinary = false; + } + }); + } + + setStorageView(view: StorageView) { + this.storageView = view; + } + + get storageViewDescriptionKey(): string { + switch (this.storageView) { + case 'composition': + return 'dashboard.storageViewCompositionHint'; + case 'activity': + return 'dashboard.storageViewActivityHint'; + default: + return 'dashboard.storageViewOverviewHint'; + } + } + + get storageUsedBytes(): number { + const storage = this.data?.storage; + if (!storage) { + return 0; + } + if (storage.total > 0 && storage.free >= 0 && storage.free <= storage.total) { + return Math.max(0, storage.total - storage.free); + } + return Math.max(0, storage.used || 0); + } + + get usedPercent(): number { + const storage = this.data?.storage; + if (!storage?.total) { + return Number(storage?.usage_percent || 0); + } + return Number(((this.storageUsedBytes / storage.total) * 100).toFixed(1)); + } + + get freePercent(): number { + const storage = this.data?.storage; + if (!storage?.total) { + return Math.max(0, 100 - this.usedPercent); + } + return Number(((storage.free / storage.total) * 100).toFixed(1)); + } + + get repositorySharePercent(): number { + const storage = this.data?.storage; + if (!storage?.total) { + return 0; + } + return Number(Math.min(100, (storage.folder_used / storage.total) * 100).toFixed(1)); + } + + get repositoryVsUsedPercent(): number { + if (!this.storageUsedBytes) { + return 0; + } + return Number(Math.min(100, ((this.data?.storage.folder_used || 0) / this.storageUsedBytes) * 100).toFixed(1)); + } + + get binarySharePercent(): number { + return Math.max(0, 100 - this.exportsSharePercent); + } + + get averageBackupsPerRouter(): string { + if (!this.data?.routers_count) { + return '0'; + } + return (this.data.total_backups / this.data.routers_count).toFixed(1); + } + + get coveragePercent(): number { + if (!this.routers.length) { + return 0; + } + const routersWithBackups = new Set(this.backups.map((item) => item.router_id)).size; + return Math.round((routersWithBackups / this.routers.length) * 100); + } + + get exportsSharePercent(): number { + if (!this.backups.length) { + return 0; + } + return Math.round((this.backups.filter((item) => item.backup_type === 'export').length / this.backups.length) * 100); + } + + get backupsLast7Days(): number { + const threshold = Date.now() - 7 * 24 * 60 * 60 * 1000; + return this.backups.filter((item) => new Date(item.created_at).getTime() >= threshold).length; + } + + get latestBackupLabel(): string { + const latest = this.latestBackup; + if (!latest) { + return this.ui.instant('dashboard.noneLabel'); + } + return latest.router_name || `#${latest.router_id}`; + } + + get latestBackupHint(): string { + const latest = this.latestBackup; + if (!latest) { + return this.ui.instant('dashboard.noActivity'); + } + return `${latest.backup_type === 'export' ? this.ui.instant('files.exportType') : this.ui.instant('files.binaryType')} · ${this.relativeAge(latest.created_at)}`; + } + + get busiestRouterLabel(): string { + const busiest = this.busiestRouter; + if (!busiest) { + return this.ui.instant('dashboard.noneLabel'); + } + return busiest.name; + } + + get busiestRouterHint(): string { + const busiest = this.busiestRouter; + if (!busiest) { + return this.ui.instant('dashboard.noActivity'); + } + return this.ui.instant('dashboard.routerSnapshotsHint', { count: busiest.count }); + } + + get activityToday(): number { + if (!this.data?.recent_logs?.length) { + return 0; + } + const today = new Date(); + return this.data.recent_logs.filter((log) => { + const value = new Date(log.timestamp); + return value.getFullYear() === today.getFullYear() && value.getMonth() === today.getMonth() && value.getDate() === today.getDate(); + }).length; + } + + get activityPageCount(): number { + const total = this.data?.recent_logs?.length || 0; + return Math.max(1, Math.ceil(total / this.activityPageSize)); + } + + get pagedRecentLogs() { + if (!this.data?.recent_logs?.length) { + return []; + } + const start = this.activityPage * this.activityPageSize; + return this.data.recent_logs.slice(start, start + this.activityPageSize); + } + + get activityRangeLabel(): string { + const total = this.data?.recent_logs?.length || 0; + if (!total) { + return '0 / 0'; + } + const start = this.activityPage * this.activityPageSize + 1; + const end = Math.min(total, start + this.activityPageSize - 1); + return `${start}-${end} / ${total}`; + } + + get storageRingBackground(): string { + const safePercent = Math.min(100, Math.max(0, this.usedPercent)); + return `conic-gradient(var(--accent) 0deg ${safePercent * 3.6}deg, rgba(129, 149, 167, 0.18) ${safePercent * 3.6}deg 360deg)`; + } + + get storageOverviewLegend(): { label: string; value: string; tone: 'accent' | 'success' | 'neutral' }[] { + return [ + { label: 'dashboard.totalDisk', value: this.formatBytes(this.data?.storage.total || 0), tone: 'accent' }, + { label: 'dashboard.freeSpace', value: this.formatBytes(this.data?.storage.free || 0), tone: 'success' }, + { label: 'dashboard.folderUsage', value: this.formatBytes(this.data?.storage.folder_used || 0), tone: 'neutral' } + ]; + } + + get storageCompositionRows(): { label: string; value: string; percent: number; tone: 'accent' | 'success' | 'neutral' }[] { + return [ + { label: 'dashboard.diskUsed', value: this.formatBytes(this.storageUsedBytes), percent: this.usedPercent, tone: 'accent' }, + { label: 'dashboard.freeSpace', value: this.formatBytes(this.data?.storage.free || 0), percent: this.freePercent, tone: 'success' }, + { label: 'dashboard.folderUsage', value: this.formatBytes(this.data?.storage.folder_used || 0), percent: this.repositoryVsUsedPercent, tone: 'neutral' } + ]; + } + + get recentBackupBars(): { label: string; value: number; percent: number; tooltip: string }[] { + const locale = this.currentLocale(); + const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short' }); + const today = new Date(); + const buckets: { date: Date; label: string; value: number }[] = []; + for (let offset = 6; offset >= 0; offset -= 1) { + const date = new Date(today); + date.setHours(0, 0, 0, 0); + date.setDate(today.getDate() - offset); + buckets.push({ date, label: formatter.format(date), value: 0 }); + } + for (const backup of this.backups) { + const created = new Date(backup.created_at); + created.setHours(0, 0, 0, 0); + const bucket = buckets.find((item) => item.date.getTime() === created.getTime()); + if (bucket) { + bucket.value += 1; + } + } + const maxValue = Math.max(...buckets.map((item) => item.value), 0); + return buckets.map((item) => ({ + label: item.label.replace('.', ''), + value: item.value, + percent: maxValue ? Math.max(14, (item.value / maxValue) * 100) : 14, + tooltip: `${item.label}: ${item.value}` + })); + } + + previousActivityPage() { + this.activityPage = Math.max(0, this.activityPage - 1); + } + + nextActivityPage() { + this.activityPage = Math.min(this.activityPageCount - 1, this.activityPage + 1); + } + + formatBytes(value: number): string { + if (!value) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let size = value; + let unit = 0; + while (size >= 1024 && unit < units.length - 1) { + size /= 1024; + unit += 1; + } + return `${size.toFixed(size >= 10 || unit === 0 ? 0 : 1)} ${units[unit]}`; + } + + formatPercent(value: number): string { + return `${Number(value || 0).toFixed(1)}%`; + } + + relativeAge(value: string): string { + const diff = Date.now() - new Date(value).getTime(); + const hours = Math.floor(diff / 3_600_000); + if (hours < 1) { + const minutes = Math.max(1, Math.floor(diff / 60_000)); + return this.ui.instant('files.minutesAgo', { value: minutes }); + } + if (hours < 24) { + return this.ui.instant('files.hoursAgo', { value: hours }); + } + const days = Math.floor(hours / 24); + return this.ui.instant('files.daysAgo', { value: days }); + } + + activityTone(message: string): 'success' | 'danger' | 'warning' | 'info' { + const normalized = message.toLowerCase(); + if (normalized.includes('fail') || normalized.includes('error')) return 'danger'; + if (normalized.includes('cleanup') || normalized.includes('retention')) return 'warning'; + if (normalized.includes('upload') || normalized.includes('email')) return 'info'; + return 'success'; + } + + activityIcon(message: string): string { + const tone = this.activityTone(message); + if (tone === 'danger') return 'pi pi-exclamation-triangle'; + if (tone === 'warning') return 'pi pi-broom'; + if (tone === 'info') return 'pi pi-send'; + return 'pi pi-check'; + } + + activityLabel(message: string): string { + const tone = this.activityTone(message); + if (tone === 'danger') return this.ui.instant('dashboard.activityFailure'); + if (tone === 'warning') return this.ui.instant('dashboard.activityMaintenance'); + if (tone === 'info') return this.ui.instant('dashboard.activityDelivery'); + return this.ui.instant('dashboard.activitySuccess'); + } + + private currentLocale(): string { + switch (this.language.current()) { + case 'no': + return 'nb-NO'; + case 'es': + return 'es-ES'; + case 'en': + return 'en-GB'; + default: + return 'pl-PL'; + } + } + + private get latestBackup(): BackupInventoryItem | undefined { + return this.backups.slice().sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0]; + } + + private get busiestRouter(): { name: string; count: number } | null { + if (!this.backups.length) { + return null; + } + const counters = new Map(); + for (const backup of this.backups) { + const entry = counters.get(backup.router_id) || { name: backup.router_name || `#${backup.router_id}`, count: 0 }; + entry.count += 1; + counters.set(backup.router_id, entry); + } + return Array.from(counters.values()).sort((a, b) => b.count - a.count)[0] || null; + } +} +''') + +settings_html = root / 'app/features/settings/settings-page.component.html' +text = settings_html.read_text() +text = text.replace('
\n ', '
\n ', 1) +text = text.replace('', '') +text = text.replace('', '') +settings_html.write_text(text) + +app_html = root / 'app/app.component.html' +app_html.write_text(''' + + +
+
+ {{ 'footer.apiOfflineTitle' | translate }} + {{ 'footer.apiOfflineMessage' | translate }} +
+ +
+ + +
+
+ + + +
+ + +
+ +
+ +
+ + {{ 'footer.authorLabel' | translate }} + {{ authorName }} + {{ authorHandle }} + + {{ 'footer.apiLabel' | translate }}: {{ apiStateLabelKey() | translate }} + {{ 'footer.apiLatencyLabel' | translate }}: {{ apiLatencyLabel() }} + {{ 'footer.apiDocs' | translate }} +
+
+
+
+ + +
+
+ +
+ +
+ + {{ 'footer.authorLabel' | translate }} + {{ authorName }} + {{ authorHandle }} + + {{ 'footer.apiLabel' | translate }}: {{ apiStateLabelKey() | translate }} + {{ 'footer.apiLatencyLabel' | translate }}: {{ apiLatencyLabel() }} + {{ 'footer.apiDocs' | translate }} +
+
+
+''') + +app_ts = root / 'app/app.component.ts' +text = app_ts.read_text() +text = text.replace(" readonly author = 'MateuszG';\n", " readonly authorName = 'Mateusz Gruszczyński';\n readonly authorHandle = '@linuxiarz.pl';\n") +app_ts.write_text(text) + +styles_path = root / 'styles.css' +styles = styles_path.read_text() +styles += ''' + +/* --- 2026 patch: dashboard storage switcher, settings dropdown, footer author --- */ +.storage-panel--enhanced { + grid-template-columns: minmax(0, 1.32fr) minmax(280px, 0.9fr); + align-items: stretch; +} + +.storage-panel__toolbar { + display: grid; + gap: 0.9rem; + margin-bottom: 1rem; +} + +.storage-panel__eyebrow { + font-size: 0.72rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--text-soft); +} + +.storage-view-switch { + display: inline-flex; + flex-wrap: wrap; + gap: 0.45rem; + padding: 0.35rem; + border-radius: 999px; + border: 1px solid var(--border-color); + background: color-mix(in srgb, var(--surface-1) 92%, transparent); +} + +.storage-view-switch__btn { + border: 0; + background: transparent; + color: var(--text-soft); + min-height: 2.4rem; + padding: 0.55rem 0.9rem; + border-radius: 999px; + display: inline-flex; + align-items: center; + gap: 0.5rem; + font: inherit; + font-size: 0.76rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + cursor: pointer; + transition: background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease; +} + +.storage-view-switch__btn:hover { + background: color-mix(in srgb, var(--surface-2) 88%, transparent); + color: var(--text-main); +} + +.storage-view-switch__btn.is-active { + background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 88%, #fff 0%), color-mix(in srgb, var(--blue) 84%, #fff 0%)); + color: #eef6ff; + box-shadow: 0 12px 24px color-mix(in srgb, var(--accent) 24%, transparent); +} + +.storage-stage { + min-width: 0; +} + +.storage-stage--overview, +.storage-stage--composition, +.storage-stage--activity { + display: grid; + gap: 1rem; +} + +.storage-stage--overview { + grid-template-columns: minmax(220px, 280px) minmax(0, 1fr); + align-items: stretch; +} + +.storage-ring-panel, +.storage-overview-side, +.storage-composition-card, +.storage-trend-card { + min-width: 0; +} + +.storage-ring-panel { + padding: 1rem; + border-radius: 22px; + border: 1px solid var(--border-color); + background: color-mix(in srgb, var(--surface-1) 92%, transparent); +} + +.storage-ring { + margin-inline: auto; +} + +.storage-legend { + display: grid; + gap: 0.65rem; +} + +.storage-legend__item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.75rem 0.85rem; + border-radius: 16px; + border: 1px solid var(--border-color); + background: color-mix(in srgb, var(--surface-0) 96%, transparent); +} + +.storage-legend__item strong { + font-family: var(--font-title); + font-size: 0.88rem; +} + +.storage-legend__item[data-tone='accent'] { + border-color: color-mix(in srgb, var(--accent) 26%, var(--border-color)); +} + +.storage-legend__item[data-tone='success'] { + border-color: color-mix(in srgb, var(--success) 28%, var(--border-color)); +} + +.storage-overview-side { + display: grid; + gap: 0.95rem; +} + +.storage-callout { + display: grid; + gap: 0.32rem; + padding: 1rem 1.05rem; + border-radius: 20px; + border: 1px solid var(--border-color); + background: linear-gradient(180deg, color-mix(in srgb, var(--surface-1) 96%, transparent), color-mix(in srgb, var(--surface-0) 95%, transparent)); +} + +.storage-callout--accent { + border-color: color-mix(in srgb, var(--accent) 26%, var(--border-color)); + box-shadow: 0 16px 28px color-mix(in srgb, var(--accent) 12%, transparent); +} + +.storage-callout strong, +.storage-mini-card strong, +.storage-trend-card__header span { + font-family: var(--font-title); + font-size: 1.08rem; + letter-spacing: 0.05em; + color: var(--text-main); +} + +.storage-callout small, +.storage-trend-card__header small { + color: var(--text-soft); + line-height: 1.55; +} + +.storage-bars--stacked { + gap: 0.85rem; +} + +.storage-mini-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.85rem; +} + +.storage-mini-grid--triple { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.storage-mini-card { + display: grid; + gap: 0.35rem; + padding: 0.9rem 1rem; + border-radius: 18px; + border: 1px solid var(--border-color); + background: color-mix(in srgb, var(--surface-1) 94%, transparent); +} + +.storage-composition-card { + padding: 1rem; + border-radius: 22px; + border: 1px solid var(--border-color); + background: color-mix(in srgb, var(--surface-1) 92%, transparent); +} + +.storage-composition-list { + display: grid; + gap: 0.85rem; +} + +.storage-composition-row { + display: grid; + gap: 0.45rem; +} + +.storage-composition-row__meta { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; +} + +.storage-composition-row__meta strong { + font-family: var(--font-title); + font-size: 0.9rem; +} + +.storage-composition-row__track { + height: 12px; + border-radius: 999px; + background: rgba(128, 145, 164, 0.14); + overflow: hidden; +} + +.storage-composition-row__track span { + display: block; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, var(--accent), var(--blue)); +} + +.storage-composition-row[data-tone='success'] .storage-composition-row__track span, +.storage-bars__track--success span { + background: linear-gradient(90deg, var(--success), #2d8d74); +} + +.storage-composition-row[data-tone='neutral'] .storage-composition-row__track span { + background: linear-gradient(90deg, #8e9cab, #b0bcc8); +} + +.storage-composition-row small { + color: var(--text-faint); +} + +.storage-trend-card { + padding: 1rem; + border-radius: 22px; + border: 1px solid var(--border-color); + background: color-mix(in srgb, var(--surface-1) 92%, transparent); +} + +.storage-trend-card__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; +} + +.storage-trend-card__header strong { + display: block; + margin-bottom: 0.2rem; +} + +.storage-trend-bars { + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + gap: 0.7rem; + align-items: end; + min-height: 220px; +} + +.storage-trend-bars__item { + display: grid; + gap: 0.45rem; + justify-items: center; +} + +.storage-trend-bars__column { + width: 100%; + min-height: 150px; + border-radius: 18px; + padding: 0.45rem; + display: flex; + align-items: flex-end; + background: linear-gradient(180deg, color-mix(in srgb, var(--surface-0) 98%, transparent), color-mix(in srgb, var(--surface-2) 96%, transparent)); + border: 1px solid var(--border-color); +} + +.storage-trend-bars__column span { + display: block; + width: 100%; + min-height: 14%; + border-radius: 14px; + background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 88%, #fff 0%), color-mix(in srgb, var(--blue) 86%, #fff 0%)); + box-shadow: 0 14px 22px color-mix(in srgb, var(--accent) 18%, transparent); +} + +.storage-trend-bars__item strong { + font-family: var(--font-title); + font-size: 0.84rem; +} + +.storage-trend-bars__item small { + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-soft); +} + +.storage-snapshot-grid { + gap: 0.75rem; + align-content: start; +} + +.storage-snapshot-card { + min-height: 88px; + padding: 0.9rem; +} + +.settings-interface-grid { + align-items: start; + row-gap: 1.1rem; +} + +.settings-interface-grid .p-dropdown { + width: 100%; +} + +.settings-floating-dropdown { + z-index: 1300 !important; +} + +.layout-footer__author { + display: inline-flex; + align-items: center; + gap: 0.6rem; + padding: 0.5rem 0.75rem; + border-radius: 999px; + border: 1px solid var(--border-color); + background: color-mix(in srgb, var(--surface-1) 94%, transparent); + text-transform: none; + letter-spacing: normal; + color: var(--text-main); +} + +.layout-footer__label { + color: var(--text-faint); +} + +.layout-footer__author strong { + font-family: var(--font-title); + font-size: 0.78rem; + letter-spacing: 0.04em; +} + +.layout-footer__author a { + font-size: 0.78rem; + letter-spacing: 0.04em; + text-transform: none; + color: var(--accent); +} + +@media (max-width: 1180px) { + .storage-panel--enhanced, + .storage-stage--overview { + grid-template-columns: 1fr; + } +} + +@media (max-width: 780px) { + .storage-mini-grid, + .storage-mini-grid--triple, + .storage-trend-bars, + .storage-snapshot-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .storage-trend-bars { + min-height: unset; + } + + .storage-trend-bars__column { + min-height: 120px; + } +} + +@media (max-width: 560px) { + .storage-view-switch { + display: grid; + grid-template-columns: 1fr; + } + + .storage-mini-grid, + .storage-mini-grid--triple, + .storage-snapshot-grid, + .storage-trend-bars { + grid-template-columns: 1fr; + } + + .storage-trend-card__header, + .storage-composition-row__meta, + .layout-footer__author { + align-items: flex-start; + flex-direction: column; + } +} +''' +styles_path.write_text(styles) + +for name, lang_data in { + 'pl.json': { + ('dashboard', 'diskUsed'): 'Zajętość dysku', + ('dashboard', 'storageViewOverview'): 'Podgląd', + ('dashboard', 'storageViewComposition'): 'Proporcje', + ('dashboard', 'storageViewActivity'): 'Aktywność 7 dni', + ('dashboard', 'storageViewOverviewHint'): 'Pierścień użycia i szybkie wskaźniki miejsca.', + ('dashboard', 'storageViewCompositionHint'): 'Udziały zajętości, wolnego miejsca i repozytorium.', + ('dashboard', 'storageViewActivityHint'): 'Nowe backupy z ostatnich 7 dni i tempo zmian.', + ('dashboard', 'storageActivityHint'): 'Liczba nowych backupów w ostatnim tygodniu.', + ('dashboard', 'storageInsightUsage'): 'Rzeczywiście zajęta przestrzeń na dysku według danych systemowych.' + }, + 'en.json': { + ('dashboard', 'diskUsed'): 'Disk used', + ('dashboard', 'storageViewOverview'): 'Overview', + ('dashboard', 'storageViewComposition'): 'Breakdown', + ('dashboard', 'storageViewActivity'): '7-day activity', + ('dashboard', 'storageViewOverviewHint'): 'Usage ring and quick space indicators.', + ('dashboard', 'storageViewCompositionHint'): 'Relative split of used, free and repository space.', + ('dashboard', 'storageViewActivityHint'): 'New backups over the last 7 days and their pace.', + ('dashboard', 'storageActivityHint'): 'Number of new backups created during the last week.', + ('dashboard', 'storageInsightUsage'): 'Actual disk occupancy calculated from current system values.' + }, + 'es.json': { + ('dashboard', 'diskUsed'): 'Disco usado', + ('dashboard', 'storageViewOverview'): 'Vista general', + ('dashboard', 'storageViewComposition'): 'Proporciones', + ('dashboard', 'storageViewActivity'): 'Actividad 7 días', + ('dashboard', 'storageViewOverviewHint'): 'Anillo de uso e indicadores rápidos de espacio.', + ('dashboard', 'storageViewCompositionHint'): 'Reparto del espacio usado, libre y del repositorio.', + ('dashboard', 'storageViewActivityHint'): 'Backups nuevos de los últimos 7 días y su ritmo.', + ('dashboard', 'storageActivityHint'): 'Número de backups nuevos creados durante la última semana.', + ('dashboard', 'storageInsightUsage'): 'Ocupación real del disco calculada con los valores actuales del sistema.' + }, + 'no.json': { + ('dashboard', 'diskUsed'): 'Brukt disk', + ('dashboard', 'storageViewOverview'): 'Oversikt', + ('dashboard', 'storageViewComposition'): 'Fordeling', + ('dashboard', 'storageViewActivity'): '7 dagers aktivitet', + ('dashboard', 'storageViewOverviewHint'): 'Bruksring og raske plassindikatorer.', + ('dashboard', 'storageViewCompositionHint'): 'Fordeling av brukt, ledig og lagringsplass for repoet.', + ('dashboard', 'storageViewActivityHint'): 'Nye sikkerhetskopier de siste 7 dagene og tempoet.', + ('dashboard', 'storageActivityHint'): 'Antall nye sikkerhetskopier opprettet den siste uken.', + ('dashboard', 'storageInsightUsage'): 'Faktisk diskbruk beregnet fra systemets nåværende verdier.' + } +}.items(): + path = root / 'assets/i18n' / name + data = json.loads(path.read_text()) + for (section, key), value in lang_data.items(): + data.setdefault(section, {})[key] = value + path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + '\n') + +print('[OK] patched') diff --git a/run_integration.py b/run_integration.py new file mode 100644 index 0000000..a09177d --- /dev/null +++ b/run_integration.py @@ -0,0 +1,100 @@ +import os +import tempfile +from pathlib import Path +from unittest.mock import patch + +root = Path('/mnt/data/appcheck/backend') +work = Path(tempfile.mkdtemp(prefix='rbmnext_')) +os.environ['DATABASE_URL'] = f"sqlite:///{work/'test.db'}" +os.environ['DATA_DIR'] = str(work/'data') +os.environ['SECRET_KEY'] = 'test-secret' +os.environ['DEFAULT_ADMIN_USERNAME'] = 'admin' +os.environ['DEFAULT_ADMIN_PASSWORD'] = 'admin' +os.environ['ALLOW_REGISTRATION'] = 'true' +os.environ['CORS_ORIGINS'] = '["http://localhost:4200"]' +os.chdir(root) +import sys +sys.path.insert(0, str(root)) + +from fastapi.testclient import TestClient +from app.main import app + +client = TestClient(app) + +results = [] + +def record(name, ok, detail=''): + results.append((name, ok, detail)) + +# health +r = client.get('/api/health') +record('health', r.status_code == 200 and r.json().get('status') == 'ok', str(r.status_code)) + +# login as default admin +r = client.post('/api/auth/login', data={'username':'admin','password':'admin'}) +record('login_form', r.status_code == 200 and 'access_token' in r.json(), str(r.text)) +token = r.json()['access_token'] +headers = {'Authorization': f'Bearer {token}'} + +# me +r = client.get('/api/auth/me', headers=headers) +record('auth_me', r.status_code == 200 and r.json()['username'] == 'admin', str(r.text)) + +# register new user and login json should fail (endpoint only form in current archive) +r = client.post('/api/auth/register', json={'username':'u1','password':'p1234'}) +record('register', r.status_code == 200 and r.json()['username'] == 'u1', str(r.text)) + +# create router +router_payload = { + 'name':'R1','host':'192.0.2.1','port':22,'ssh_user':'admin','ssh_password':'pass','ssh_key':'' +} +r = client.post('/api/routers', json=router_payload, headers=headers) +record('create_router', r.status_code == 200 and r.json()['name'] == 'R1', str(r.text)) +router_id = r.json()['id'] + +# list routers +r = client.get('/api/routers', headers=headers) +record('list_routers', r.status_code == 200 and len(r.json()) == 1, str(r.text)) + +# dashboard +r = client.get('/api/dashboard', headers=headers) +record('dashboard', r.status_code == 200 and r.json()['routers_count'] == 1, str(r.text)) + +# fake router tests and backups +with patch('app.services.router_service.router_service.test_connection', return_value={'model':'hAP ax3','uptime':'1d','hostname':'r1'}): + r = client.get(f'/api/routers/{router_id}/test-connection', headers=headers) + record('test_connection_route', r.status_code == 200 and r.json()['hostname'] == 'r1', str(r.text)) + +with patch('app.services.router_service.router_service.export', return_value='/system identity set name=r1\n'): + r = client.post(f'/api/backups/router/{router_id}/export', headers=headers, json={}) + record('export_router', r.status_code == 200 and r.json()['backup_type'] == 'export', str(r.text)) + export_id = r.json()['id'] + +with patch('app.services.router_service.router_service.binary_backup', side_effect=lambda router, base_name, path, key: Path(path).write_bytes(b'binary')): + r = client.post(f'/api/backups/router/{router_id}/binary', headers=headers, json={}) + record('binary_backup', r.status_code == 200 and r.json()['backup_type'] == 'binary', str(r.text)) + binary_id = r.json()['id'] + +# view export +r = client.get(f'/api/backups/{export_id}/view', headers=headers) +record('view_export', r.status_code == 200 and 'content' in r.json(), str(r.text)) + +# download requires auth header at HTTP level; endpoint itself works with auth header +r = client.get(f'/api/backups/{binary_id}/download', headers=headers) +record('download_with_auth', r.status_code == 200 and r.content == b'binary', str(r.status_code)) + +# diff html endpoint works with auth header +r = client.get(f'/api/backups/{export_id}/diff/{export_id}/html', headers=headers) +record('diff_html_with_auth', r.status_code == 200 and '/dev/null; then + kill "${BACKEND_PID}" 2>/dev/null || true + fi +} +trap cleanup EXIT INT TERM + +read_env_value() { + local key="$1" + local file="$2" + if [[ -f "$file" ]]; then + local line + line=$(grep -E "^${key}=" "$file" | tail -n 1 || true) + if [[ -n "$line" ]]; then + echo "${line#*=}" + return 0 + fi + fi + return 1 +} + +if [[ ! -d "${BACKEND_VENV}" ]]; then + "${PYTHON_BIN}" -m venv "${BACKEND_VENV}" +fi + +# shellcheck disable=SC1091 +source "${BACKEND_VENV}/bin/activate" +python -m pip install --upgrade pip +python -m pip install -r "${BACKEND_DIR}/requirements.txt" + +if [[ ! -f "${ENV_FILE}" ]] && [[ -f "${ENV_EXAMPLE_FILE}" ]]; then + cp "${ENV_EXAMPLE_FILE}" "${ENV_FILE}" +fi + +DEFAULT_ADMIN_USERNAME="$(read_env_value DEFAULT_ADMIN_USERNAME "${ENV_FILE}" || read_env_value DEFAULT_ADMIN_USERNAME "${ENV_EXAMPLE_FILE}" || echo admin)" +DEFAULT_ADMIN_PASSWORD="$(read_env_value DEFAULT_ADMIN_PASSWORD "${ENV_FILE}" || read_env_value DEFAULT_ADMIN_PASSWORD "${ENV_EXAMPLE_FILE}" || echo admin)" + +cd "${BACKEND_DIR}" +PYTHONPATH="${BACKEND_DIR}" uvicorn app.main:app --reload --reload-dir app --host 127.0.0.1 --port 8000 > "${BACKEND_LOG}" 2>&1 & +BACKEND_PID=$! + +for _ in $(seq 1 30); do + if python - <<'PY' +import sys +from urllib.request import urlopen +try: + with urlopen('http://127.0.0.1:8000/api/health', timeout=1) as response: + sys.exit(0 if response.status == 200 else 1) +except Exception: + sys.exit(1) +PY + then + break + fi + sleep 1 +done + +if ! python - <<'PY' +import sys +from urllib.request import urlopen +try: + with urlopen('http://127.0.0.1:8000/api/health', timeout=1) as response: + sys.exit(0 if response.status == 200 else 1) +except Exception: + sys.exit(1) +PY +then + echo "Backend failed to become ready. Log: ${BACKEND_LOG}" + tail -n 100 "${BACKEND_LOG}" || true + exit 1 +fi + +echo "Backend: http://127.0.0.1:8000" +echo "API docs: http://127.0.0.1:8000/docs" +echo "Frontend: http://127.0.0.1:4200" +echo "Default login: ${DEFAULT_ADMIN_USERNAME} / ${DEFAULT_ADMIN_PASSWORD}" + +cd "${FRONTEND_DIR}" +if [[ ! -d node_modules ]]; then + npm install +fi + +npm run start diff --git a/start_prod.sh b/start_prod.sh new file mode 100755 index 0000000..11dec12 --- /dev/null +++ b/start_prod.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${ROOT_DIR}" + +if [[ ! -f .env ]] && [[ -f .env.example ]]; then + cp .env.example .env +fi + +if grep -q '^DEFAULT_ADMIN_PASSWORD=admin$' .env 2>/dev/null; then + echo "Warning: DEFAULT_ADMIN_PASSWORD is still set to admin in .env" +fi + +docker compose up --build