commit ff7dbcb4e4e3ca32b0019b53c88e627cc2b34dbe Author: Mateusz Gruszczyński Date: Sun Apr 12 21:26:12 2026 +0200 first commit 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