From 791f4526f974cdf4ea7d6446a0e332f6aa0151f1 Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Mon, 27 Jul 2026 18:44:47 +0200 Subject: [PATCH] chore(v2): establish protocol and test foundation --- .gitignore | 4 +- .nvmrc | 1 + .python-version | 1 + Makefile | 28 + backend/alembic.ini | 5 - backend/alembic/README | 1 - backend/alembic/env.py | 47 - backend/alembic/script.py.mako | 28 - .../app/__pycache__/database.cpython-314.pyc | Bin 1416 -> 0 bytes backend/app/__pycache__/main.cpython-314.pyc | Bin 4302 -> 0 bytes .../app/__pycache__/models.cpython-314.pyc | Bin 8678 -> 0 bytes .../app/__pycache__/schemas.cpython-314.pyc | Bin 11968 -> 0 bytes backend/app/database.py | 17 - backend/app/main.py | 60 - backend/app/models.py | 137 - .../__pycache__/backups.cpython-314.pyc | Bin 3245 -> 0 bytes .../__pycache__/dashboard.cpython-314.pyc | Bin 3632 -> 0 bytes .../__pycache__/executions.cpython-314.pyc | Bin 2312 -> 0 bytes .../routers/__pycache__/jobs.cpython-314.pyc | Bin 8168 -> 0 bytes .../__pycache__/settings.cpython-314.pyc | Bin 3314 -> 0 bytes .../__pycache__/sources.cpython-314.pyc | Bin 5189 -> 0 bytes backend/app/routers/backups.py | 34 - backend/app/routers/dashboard.py | 54 - backend/app/routers/executions.py | 23 - backend/app/routers/jobs.py | 101 - backend/app/routers/settings.py | 43 - backend/app/routers/sources.py | 61 - backend/app/schemas.py | 143 - .../backup/__pycache__/engine.cpython-314.pyc | Bin 7721 -> 0 bytes backend/backup/adapters/__init__.py | 17 - .../__pycache__/__init__.cpython-314.pyc | Bin 1055 -> 0 bytes .../adapters/__pycache__/base.cpython-314.pyc | Bin 3525 -> 0 bytes .../__pycache__/local.cpython-314.pyc | Bin 4711 -> 0 bytes backend/backup/adapters/base.py | 39 - backend/backup/adapters/database.py | 74 - backend/backup/adapters/local.py | 47 - backend/backup/adapters/ssh.py | 92 - backend/backup/engine.py | 138 - backend/backup/retention.py | 90 - backend/backup/scheduler.py | 81 - backend/backup_tool.db | Bin 53248 -> 0 bytes backend/pyproject.toml | 91 +- backend/src/backup_tool/__init__.py | 3 + backend/src/backup_tool/cli.py | 23 + .../src/backup_tool/py.typed | 0 .../conftest.cpython-314-pytest-9.0.3.pyc | Bin 3716 -> 0 bytes .../test_engine.cpython-314-pytest-9.0.3.pyc | Bin 13010 -> 0 bytes .../test_jobs.cpython-314-pytest-9.0.3.pyc | Bin 23109 -> 0 bytes .../test_sources.cpython-314-pytest-9.0.3.pyc | Bin 14411 -> 0 bytes backend/tests/conftest.py | 33 - backend/tests/test_engine.py | 82 - backend/tests/test_jobs.py | 205 - backend/tests/test_sources.py | 88 - .../repository/v1/capabilities-v2.0.json | 14 + contracts/repository/v1/error-codes.json | 22 + .../repository/v1/execution-transitions.json | 10 + contracts/repository/v1/fault-points.json | 14 + .../v1/fixtures/invalid-manifest.json | 1 + .../v1/fixtures/invalid-repository.json | 1 + .../v1/fixtures/valid-manifest.json | 1 + .../v1/fixtures/valid-repository.json | 1 + contracts/repository/v1/manifest.schema.json | 67 + contracts/repository/v1/normalized-paths.json | 9 + .../repository/v1/repository.schema.json | 24 + frontend/package-lock.json | 3670 +++++++++++++++++ frontend/package.json | 43 +- frontend/src/App.tsx | 33 - frontend/src/api/client.ts | 62 - frontend/src/api/generated/.gitkeep | 0 frontend/src/app/App.tsx | 16 + frontend/src/components/Layout.tsx | 80 - frontend/src/main.tsx | 25 +- frontend/src/pages/Backups.tsx | 238 -- frontend/src/pages/Dashboard.tsx | 141 - frontend/src/pages/Settings.tsx | 205 - frontend/src/vite-env.d.ts | 1 + tests/contract/test_no_v1.py | 3 +- tests/contract/test_repository_format.py | 1 - tests/e2e/.gitkeep | 0 tests/fault/.gitkeep | 0 tests/fixtures/.gitkeep | 0 tests/integration/.gitkeep | 0 tests/performance/.gitkeep | 0 tests/security/.gitkeep | 0 tests/unit/.gitkeep | 0 tools/forbidden_v1_scan.py | 69 + 86 files changed, 4060 insertions(+), 2582 deletions(-) create mode 100644 .nvmrc create mode 100644 .python-version create mode 100644 Makefile delete mode 100644 backend/alembic.ini delete mode 100644 backend/alembic/README delete mode 100644 backend/alembic/env.py delete mode 100644 backend/alembic/script.py.mako delete mode 100644 backend/app/__pycache__/database.cpython-314.pyc delete mode 100644 backend/app/__pycache__/main.cpython-314.pyc delete mode 100644 backend/app/__pycache__/models.cpython-314.pyc delete mode 100644 backend/app/__pycache__/schemas.cpython-314.pyc delete mode 100644 backend/app/database.py delete mode 100644 backend/app/main.py delete mode 100644 backend/app/models.py delete mode 100644 backend/app/routers/__pycache__/backups.cpython-314.pyc delete mode 100644 backend/app/routers/__pycache__/dashboard.cpython-314.pyc delete mode 100644 backend/app/routers/__pycache__/executions.cpython-314.pyc delete mode 100644 backend/app/routers/__pycache__/jobs.cpython-314.pyc delete mode 100644 backend/app/routers/__pycache__/settings.cpython-314.pyc delete mode 100644 backend/app/routers/__pycache__/sources.cpython-314.pyc delete mode 100644 backend/app/routers/backups.py delete mode 100644 backend/app/routers/dashboard.py delete mode 100644 backend/app/routers/executions.py delete mode 100644 backend/app/routers/jobs.py delete mode 100644 backend/app/routers/settings.py delete mode 100644 backend/app/routers/sources.py delete mode 100644 backend/app/schemas.py delete mode 100644 backend/backup/__pycache__/engine.cpython-314.pyc delete mode 100644 backend/backup/adapters/__init__.py delete mode 100644 backend/backup/adapters/__pycache__/__init__.cpython-314.pyc delete mode 100644 backend/backup/adapters/__pycache__/base.cpython-314.pyc delete mode 100644 backend/backup/adapters/__pycache__/local.cpython-314.pyc delete mode 100644 backend/backup/adapters/base.py delete mode 100644 backend/backup/adapters/database.py delete mode 100644 backend/backup/adapters/local.py delete mode 100644 backend/backup/adapters/ssh.py delete mode 100644 backend/backup/engine.py delete mode 100644 backend/backup/retention.py delete mode 100644 backend/backup/scheduler.py delete mode 100644 backend/backup_tool.db create mode 100644 backend/src/backup_tool/__init__.py create mode 100644 backend/src/backup_tool/cli.py rename backup_tool.db => backend/src/backup_tool/py.typed (100%) delete mode 100644 backend/tests/__pycache__/conftest.cpython-314-pytest-9.0.3.pyc delete mode 100644 backend/tests/__pycache__/test_engine.cpython-314-pytest-9.0.3.pyc delete mode 100644 backend/tests/__pycache__/test_jobs.cpython-314-pytest-9.0.3.pyc delete mode 100644 backend/tests/__pycache__/test_sources.cpython-314-pytest-9.0.3.pyc delete mode 100644 backend/tests/conftest.py delete mode 100644 backend/tests/test_engine.py delete mode 100644 backend/tests/test_jobs.py delete mode 100644 backend/tests/test_sources.py create mode 100644 contracts/repository/v1/capabilities-v2.0.json create mode 100644 contracts/repository/v1/error-codes.json create mode 100644 contracts/repository/v1/execution-transitions.json create mode 100644 contracts/repository/v1/fault-points.json create mode 100644 contracts/repository/v1/fixtures/invalid-manifest.json create mode 100644 contracts/repository/v1/fixtures/invalid-repository.json create mode 100644 contracts/repository/v1/fixtures/valid-manifest.json create mode 100644 contracts/repository/v1/fixtures/valid-repository.json create mode 100644 contracts/repository/v1/manifest.schema.json create mode 100644 contracts/repository/v1/normalized-paths.json create mode 100644 contracts/repository/v1/repository.schema.json create mode 100644 frontend/package-lock.json delete mode 100644 frontend/src/App.tsx delete mode 100644 frontend/src/api/client.ts create mode 100644 frontend/src/api/generated/.gitkeep create mode 100644 frontend/src/app/App.tsx delete mode 100644 frontend/src/components/Layout.tsx delete mode 100644 frontend/src/pages/Backups.tsx delete mode 100644 frontend/src/pages/Dashboard.tsx delete mode 100644 frontend/src/pages/Settings.tsx create mode 100644 frontend/src/vite-env.d.ts create mode 100644 tests/e2e/.gitkeep create mode 100644 tests/fault/.gitkeep create mode 100644 tests/fixtures/.gitkeep create mode 100644 tests/integration/.gitkeep create mode 100644 tests/performance/.gitkeep create mode 100644 tests/security/.gitkeep create mode 100644 tests/unit/.gitkeep create mode 100644 tools/forbidden_v1_scan.py diff --git a/.gitignore b/.gitignore index 1a63b77..77846ab 100644 --- a/.gitignore +++ b/.gitignore @@ -50,7 +50,6 @@ Thumbs.db # Frontend frontend/node_modules/ frontend/dist/ -frontend/package-lock.json frontend/yarn.lock frontend/pnpm-lock.yaml @@ -60,8 +59,11 @@ logs/ # Testing .pytest_cache/ +.mypy_cache/ +.ruff_cache/ .coverage htmlcov/ # Backup tool specific backups/ +.pi-subagents/ diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..7377d13 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.17.1 diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..7eebfaf --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12.11 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dea06c4 --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +PYTHON ?= .venv/bin/python +PIP ?= .venv/bin/pip +NPM ?= npm + +.PHONY: install test-fast lint typecheck frontend-build check-v1-absent check + +install: + $(PIP) install -e 'backend[dev]' + $(NPM) --prefix frontend ci + +test-fast: + $(PYTHON) -m pytest tests/unit tests/contract -q + +lint: + $(PYTHON) -m ruff check --config backend/pyproject.toml backend/src tests tools + $(PYTHON) -m ruff format --check --config backend/pyproject.toml backend/src tests tools + +typecheck: + $(PYTHON) -m mypy --config-file backend/pyproject.toml + $(NPM) --prefix frontend run typecheck + +frontend-build: + $(NPM) --prefix frontend run build + +check-v1-absent: + $(PYTHON) tools/forbidden_v1_scan.py . + +check: check-v1-absent test-fast lint typecheck frontend-build diff --git a/backend/alembic.ini b/backend/alembic.ini deleted file mode 100644 index cac063f..0000000 --- a/backend/alembic.ini +++ /dev/null @@ -1,5 +0,0 @@ -[alembic] -script_location = alembic -prepend_sys_path = . -version_path_separator = os -sqlalchemy.url = sqlite+aiosqlite:///./backup_tool.db diff --git a/backend/alembic/README b/backend/alembic/README deleted file mode 100644 index 98e4f9c..0000000 --- a/backend/alembic/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/backend/alembic/env.py b/backend/alembic/env.py deleted file mode 100644 index 58b16a0..0000000 --- a/backend/alembic/env.py +++ /dev/null @@ -1,47 +0,0 @@ -import asyncio -from logging.config import fileConfig -from sqlalchemy import pool -from sqlalchemy.engine import Connection -from sqlalchemy.ext.asyncio import async_engine_from_config -from alembic import context -from app.models import Base - -config = context.config -if config.config_file_name is not None: - fileConfig(config.config_file_name) - -target_metadata = Base.metadata - -def run_migrations_offline() -> None: - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - ) - with context.begin_transaction(): - context.run_migrations() - -def do_run_migrations(connection: Connection) -> None: - context.configure(connection=connection, target_metadata=target_metadata) - with context.begin_transaction(): - context.run_migrations() - -async def run_async_migrations() -> None: - connectable = async_engine_from_config( - config.get_section(config.config_ini_section, {}), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) - async with connectable.connect() as connection: - await connection.run_sync(do_run_migrations) - await connectable.dispose() - -def run_migrations_online() -> None: - asyncio.run(run_async_migrations()) - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako deleted file mode 100644 index 1101630..0000000 --- a/backend/alembic/script.py.mako +++ /dev/null @@ -1,28 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision: str = ${repr(up_revision)} -down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)} -branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} -depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} - - -def upgrade() -> None: - """Upgrade schema.""" - ${upgrades if upgrades else "pass"} - - -def downgrade() -> None: - """Downgrade schema.""" - ${downgrades if downgrades else "pass"} diff --git a/backend/app/__pycache__/database.cpython-314.pyc b/backend/app/__pycache__/database.cpython-314.pyc deleted file mode 100644 index 13e9307fa114ba6a5be730c0da07d56f1ae01c18..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1416 zcmb7EPi)&%82_H_*h!iuX<8^1D```-5>@%5sZ$33QcP8@8f zq(zz{ZWD)rI53b9J8C$4lKSBAA%Z5n0}5FLm+jmlDR3@V;9b5TxI&WI_{Bg$bj2i; zU@7XCc)TwSJ;pv#1SK$wUD@-l~m9R1}$M?eANJ*RoKgS(ZBBS6&6*2|s!TT)aY?}9mb(?A~RW)b3s;jiRZd9ok zk(stk9mlY$UZUr7dTK?xLhUs1;$^CvnytCU7FE|Y2eV$2SL6kG`Lw#K6nCC?-Zc%E z&S-|!LoelWxomDt)34NPs%u$hw!G#Am)|O?r(Z3+=>;xpCKeG~yH2sZK=lpl^)%;+ z*u`;FFHRdZ!=|cLRduUUF0j`9$E$fSeG%8b-8B{KVmQu1TZs`lBejkbIUQ+DiwtLY zX3rBAb3$B*gj%PFJRfbn7)Jc9z%eO?(3_5VoaJ~=}Qg5m*B2>A&@zrwjaIQI+8+>H%g-?_GP{pz)=H>&OIN+-73 zjNA(yyK(6jw;R44-i_Xl-r4zXu9IIr$e-WKpYP-^wpF^9r~9GHP4UN(<2Sc%neEjJ Z`y&_IFmz8E?MP!y;SV7|Qua7@{0C||JL>=d diff --git a/backend/app/__pycache__/main.cpython-314.pyc b/backend/app/__pycache__/main.cpython-314.pyc deleted file mode 100644 index d2a6ce25cee7a520f53c47c107961aafa8240c81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4302 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 N_-nC$b9kmp`(MtjVqX9N diff --git a/backend/app/__pycache__/schemas.cpython-314.pyc b/backend/app/__pycache__/schemas.cpython-314.pyc deleted file mode 100644 index 16446a8bf8d4ba7d7a4a938fc89528aee911f2c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11968 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 Li_UY5OUi!(-fQK~ diff --git a/backend/app/database.py b/backend/app/database.py deleted file mode 100644 index 4695d8b..0000000 --- a/backend/app/database.py +++ /dev/null @@ -1,17 +0,0 @@ -import os -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker -from sqlalchemy.orm import declarative_base - -DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite+aiosqlite:///./backup_tool.db") - -engine = create_async_engine(DATABASE_URL, echo=os.environ.get("SQL_ECHO", "false").lower() == "true") -AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) - -Base = declarative_base() - -async def get_db(): - async with AsyncSessionLocal() as session: - try: - yield session - finally: - await session.close() diff --git a/backend/app/main.py b/backend/app/main.py deleted file mode 100644 index e9ae032..0000000 --- a/backend/app/main.py +++ /dev/null @@ -1,60 +0,0 @@ -import os -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware -from fastapi.staticfiles import StaticFiles -from fastapi.responses import FileResponse -from contextlib import asynccontextmanager -from app.database import engine, Base -from app import models # noqa: F401 - registers models with Base.metadata -from app.routers import sources, jobs, executions, backups, settings, dashboard -from backup.scheduler import backup_scheduler - -@asynccontextmanager -async def lifespan(app: FastAPI): - async with engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - backup_scheduler.start() - await backup_scheduler.sync_schedules() - yield - backup_scheduler.shutdown() - -app = FastAPI( - title="Backup Tool API", - version="0.1.0", - lifespan=lifespan -) - -app.add_middleware( - CORSMiddleware, - allow_origins=os.environ.get("CORS_ORIGINS", "http://localhost:3000").split(","), - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -app.include_router(sources.router) -app.include_router(jobs.router) -app.include_router(executions.router) -app.include_router(backups.router) -app.include_router(settings.router) -app.include_router(dashboard.router) - -@app.get("/api/health") -async def health_check(): - return {"status": "healthy"} - -def main(): - import uvicorn - uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True) - -# Static files for production -frontend_dist = os.path.join(os.path.dirname(__file__), "../../frontend/dist") -if os.path.exists(frontend_dist): - app.mount("/assets", StaticFiles(directory=os.path.join(frontend_dist, "assets")), name="assets") - - @app.get("/{path:path}") - async def serve_frontend(path: str): - index_file = os.path.join(frontend_dist, "index.html") - if os.path.exists(index_file): - return FileResponse(index_file) - return {"detail": "Frontend not built"} diff --git a/backend/app/models.py b/backend/app/models.py deleted file mode 100644 index 83c1965..0000000 --- a/backend/app/models.py +++ /dev/null @@ -1,137 +0,0 @@ -from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, ForeignKey, JSON -from sqlalchemy.orm import relationship -from datetime import datetime, timezone -from .database import Base - - -class Source(Base): - __tablename__ = "sources" - - id = Column(Integer, primary_key=True) - name = Column(String, nullable=False) - type = Column(String, nullable=False) # local, ssh, database - config = Column(JSON, default=lambda: {}) - created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) - updated_at = Column( - DateTime, - default=lambda: datetime.now(timezone.utc), - onupdate=lambda: datetime.now(timezone.utc), - ) - - jobs = relationship("Job", back_populates="source", cascade="all, delete-orphan") - - def __repr__(self): - return f"" - - -class Job(Base): - __tablename__ = "jobs" - - id = Column(Integer, primary_key=True) - name = Column(String, nullable=False) - source_id = Column(Integer, ForeignKey("sources.id"), nullable=False, index=True) - strategy = Column(String, nullable=False, default="full") # full, incremental - destination_path = Column(String, nullable=False) - exclude_patterns = Column(JSON, default=lambda: []) - enabled = Column(Boolean, default=True) - created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) - updated_at = Column( - DateTime, - default=lambda: datetime.now(timezone.utc), - onupdate=lambda: datetime.now(timezone.utc), - ) - - source = relationship("Source", back_populates="jobs") - schedule = relationship( - "Schedule", back_populates="job", uselist=False, cascade="all, delete-orphan" - ) - executions = relationship( - "JobExecution", back_populates="job", cascade="all, delete-orphan" - ) - - def __repr__(self): - return f"" - - -class Schedule(Base): - __tablename__ = "schedules" - - id = Column(Integer, primary_key=True) - job_id = Column( - Integer, ForeignKey("jobs.id"), unique=True, nullable=False, index=True - ) - cron_expression = Column(String, nullable=False) - enabled = Column(Boolean, default=True) - created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) - updated_at = Column( - DateTime, - default=lambda: datetime.now(timezone.utc), - onupdate=lambda: datetime.now(timezone.utc), - ) - - job = relationship("Job", back_populates="schedule") - - def __repr__(self): - return f"" - - -class JobExecution(Base): - __tablename__ = "job_executions" - - id = Column(Integer, primary_key=True) - job_id = Column(Integer, ForeignKey("jobs.id"), nullable=False, index=True) - status = Column( - String, nullable=False, default="pending" - ) # pending, running, success, failed, cancelled - started_at = Column(DateTime, nullable=True) - completed_at = Column(DateTime, nullable=True) - bytes_processed = Column(Integer, default=0) - bytes_backed_up = Column(Integer, default=0) - error_message = Column(Text, nullable=True) - triggered_by = Column(String, nullable=False) # manual, schedule - - job = relationship("Job", back_populates="executions") - backups = relationship( - "Backup", back_populates="execution", cascade="all, delete-orphan" - ) - - def __repr__(self): - return f"" - - -class Backup(Base): - __tablename__ = "backups" - - id = Column(Integer, primary_key=True) - execution_id = Column( - Integer, ForeignKey("job_executions.id"), nullable=False, index=True - ) - storage_path = Column(String, nullable=False) - size_bytes = Column(Integer, default=0) - checksum = Column(String, nullable=True) - type = Column(String, nullable=False) # full, incremental - parent_backup_id = Column( - Integer, ForeignKey("backups.id"), nullable=True, index=True - ) - created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) - - execution = relationship("JobExecution", back_populates="backups") - parent_backup = relationship("Backup", remote_side=[id]) - - def __repr__(self): - return f"" - - -class Setting(Base): - __tablename__ = "settings" - - key = Column(String, primary_key=True) - value = Column(Text, nullable=True) - updated_at = Column( - DateTime, - default=lambda: datetime.now(timezone.utc), - onupdate=lambda: datetime.now(timezone.utc), - ) - - def __repr__(self): - return f"" diff --git a/backend/app/routers/__pycache__/backups.cpython-314.pyc b/backend/app/routers/__pycache__/backups.cpython-314.pyc deleted file mode 100644 index 7ba129c954258e557e8f15d3cba825b2fe389fcd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3245 zcmds3&2JM&6rb5$f7_do5Yi@)Zvrk5LrB7>KnX2zN}8GlSv@4U8+((OI<`BzQ4>%r zLn{^31Fcl4mC6BC9Ei$+N=Q9$A^Zi2Oyy)$1XVB1Eomewdg^;)Zvx??m-f<;_PsZ6 zX5PGc`+IL@hZ^t_XqP&rv&S5S{DvL7L0`0$_bDMca)xNMkA$g3!;&S1WlOF{oz$m< z9hO7La-TEovRpz|`rKiUn&-EPd4EGz4o@W|-Mb+B9P0nS_ywV?;|) zCHQ}2_*Png+a=Z7Jfjr8RD&)V6B#UZRn+u=^y<$^8y5$F)n;@&tjAhwB^ZgC}Hm~rkh z<9bSGCYSY8Dkw7yHFpANCQ=p?!N*9oHZ-s*9Py_Bl(7=xSOchV2lF(LMF{qz%BpV; zes%Eb$o1%V=|WxSTwUkwkwRDRTvuAYH3 zP@Cq|ro7sc>z(&iYEpa)Swix0fNZCeUMZAlc2$%|jYfUU? zh*bxzSVnSPUP5+rv*@vE7>=)Q3Y}HR{tlfIWa)no*(li`Cd3A%&7T7&0PI!Z&dO1F z8~BZFRK>C}xC&*BYLX_~1$~T0WdYS40;eCU~y-{u=Hdq=wh$K9#4VV@zI#U3@#-TyrPKVSSD@6GAxz`&hXBgR#-DGLQJ!I zAlz9(E~2D$71Um=u*DI!2Szr)XTAsI1(0r^t82gA_0xeL58U<4?d{9gy`8T-Q3#v> zFb66Nf!4V|Yd)|&cl@Dm!!K&>4SBZX_p%wwVidr`fE)yWv_bws3*)MvpKy4KM;{O`8^hE_Y zD!(ja>Isx{1kk!>m+bW!R!=lVbKvC)W&8|$P4M|ejyMMx<`&zI1YalIH4g6tebhLo zmeK}pbOXwH(6cT(*iKxt3rQOi%mz#)oXwr_%=mcHVj)nP&t^iPIK@teeJ}&l&b$QV z-%N%_UjLQg<=~gc^WNHmQah{E&KEUb>?kNzvr5&xziPI6+pKTbLrzeQX)6IfwH}nWQl#6E4g@!G*N6^P zCYxk^1`{7N~ zme4JIST_xZuW{~0m3ViV#hciN3KPxiCifSA;wjOEJGb~$l%W`ji&c~&)`_m+f0!mB z3yasV`!HCYNEt`iF&M)4jrl#0C7DwCkaW(G&Ics)fNXg{svnSgA+-JKsra_-mgmO# z`<_EN=OgO+yyim9rDNBWYlA;GwC8H_bnn6&dv3M<5PFPntQDC^9ZM2OOI{+m#pHn- zhjHHih5bkGI`5P{#)}Q-9axe;T2gV|mJ3@heUhhj3x|5{cHKGp7|RIGssBR)Nu1$t DsPk_{ diff --git a/backend/app/routers/__pycache__/dashboard.cpython-314.pyc b/backend/app/routers/__pycache__/dashboard.cpython-314.pyc deleted file mode 100644 index e4f4bf2c2803e0dca1e702aa0c98be0530c1519f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3632 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 Tzpz0J7KYGrc9WN}# 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 Hvd#WC?VPfD diff --git a/backend/app/routers/__pycache__/settings.cpython-314.pyc b/backend/app/routers/__pycache__/settings.cpython-314.pyc deleted file mode 100644 index 8193bdbbf841c2dde70f8d213d05aa30f2da01a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3314 zcmd5;-ESMm5#PN#^7u`W)R&d&%O7&ja>O8#@?pd=1ho-~xGH;2=st)c&g99WLy^qx zF_9c3Wj=t&NCKy5(HMyX_`wC@_QgR8w2zJR4_J@`+J^{s(EzRAsH#Zex6UkiqQkgO z^OgZ_c6N7e?q>EkvojtF`UuePrlnI?T!j1`Cl0|}(Do7~q(+`2I!%%UwP-@Jq=anA z!?-3Tm4wT330Y3M6RM>OSxI^lUdtp$)qbbrR12_GD2FX;j2os2X}GX5#}XQWB02m6Q~>LbOj zBAse35-avU?1nSp`&jA%OOXys-C$|FW%jN?zrUp@Sc-L6>eajV5UuAnTG3?NJCjQ4 z<*H>eTm6YyF-v)F2NTaclltjO&a7C4a_JJa12cTJlsjf}4s(s#Zf+LMoCT6{w7_w6 zpERv({sc(sG1IaNrIVV({O}1o*qT3E$s3k=Iv6)9g*b2OaNFZZnq<2x%v>z2*otMG zcDbB4i#ZH;E93+ZPljiEFR&U(ljo$z-XtUsYd<2tpi5Mz3$W9q zI0dJr+xU(x9EJWwvZs9BuBlkHnYC%Vyjtnk(@NE1v=vdHjTtd%jwE&?=XU zLWT%|3zaw%LE}5FOjK6wKsIZXN@WWi&1P927!AO~W58xRr7ohBb#4&~ZTviX%B#bjCTNlT)C#LTmC-myc9vnx<4oyFEH+ zO?lgEj^ylBq_nh$sO0G0X}Lf(I&2a55x|pm5Wt3D4I#rH2X-5LzZL6w|In`wonO3o{KN7_Y@!~V zs6BN(+;jHTGp}xhhwI@z>)|~c;eEBlb$_h(^DP$%#qaPiy#4y_89%*9XS~Yq_kncU z9e??GbM=*P;0FVTzq9=iuF79>qZS=kg)6{D3eT;BcCaI-<^8R_lOasb1%!eio$8Vr zzVKVX{^>}of{Z|BjVzX{r91@N&mqgIJez~MV7v3CWfY2<$}qBb0n+u-lF3Xq0;_hk z3C?V}WM<1OTY`~c!fA@w7pKSuQB!pwi4YmvfvX>DTx*N{7`7tt@LvM^7Fg>;lj}p1 zf13OA)1N&3*U9y%r#6P>He!i-FaZD#b=O1t*F*a^LRxL^ra$^+=z)*rt5b8=LW!#$ zfO51pzvU&NB)!8C39rvcGY`|x6gu;e@|o)alt;UOe*?^j5CP^VK=?jj*5x(^;t+j^ z1_HYovQW$0Y*iHRCeURziV9fSz7y?u9DEEIqCJ9W2lk~m(2!yLs(ZBIg7M#O;_cZ| zL24o&{cvap`F{W;w2&|TV2}^UzAy5Xw6cSIO(5UtcF1v>!U%SdPMut+D3DcH$~q{m zIC(fseing>eh({YrG#-41Xd8H9pvO@XD$yD(8u%dp!Qeq@rELDb`5TaQFweJccpo_ z|3|=z?RwEDR!z;AM7CN%0@nKyL08`<$`6a&<()Vv81`0Y6nmaZxoTF z3$FLu;1JLVCK<8$hFu2LEjkkTcB>FMly!QRLz|p(`&10)t9}{y?CZGjq zc9=3$P;t$R@@dPJs>Se(E)kLW&91=n1lX*RK-srjb<$=qAYu>{(_bsZvHSpTN+Uy18LgtH+$=bjMow_mp@a0GT zc;strz_w)~O*bTv8a^U5{Y P)z?_Lgyz2_r-uC($Psf7 diff --git a/backend/app/routers/__pycache__/sources.cpython-314.pyc b/backend/app/routers/__pycache__/sources.cpython-314.pyc deleted file mode 100644 index 3979d161792ee7eaf19dac5215c0654627260d20..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5189 zcmds5O>7&-6`t8${{Kj#{v}$}pJh|F#l*i->^h*-%9QO2EbXu@Aeo@al|+{!+1X_x zCumDM1uUb7xJZE-Z4N%TfQ{ym00B}U7yn#(=s^PAP+Hiqi{?<@sH$+_qKCdW%avqW zmXkJzq66%kH$VHhGxNRgz2Ppm%TAy??i0>^Y$fEc*f5HzqOAUl5>g;%NQ91&5LIbN zP=%1FiazWU#-xx*HE~@WGlwjyh3nFoHDpt5TsMu`Lk`u!b@P}r*Auv-^-v85WYR9j`K=>=a{x1}WySgNnG)C`th z-S9CLHL|s(2C&pvWvLY`9oWp0u`AE*Q4?5cuCme|Y3U*U*2iebFKV`6cr235t1{Cp z-i&kQsdK# zMgw5h&15B)%_wqgHk*{w2?Rm~3Y+O~)X18`UFq55?1M##x7s#>fIasU`I2%v=D4*M-X0vIo z!(PDxaskG-PcarUkUOtAW3hN9lT~49M94XqH%ANNNt2!AXq1L6#_aXl zaHFtoz0c5$v78VUdWeOb>^&f+sGpAeO`1iXlM{#_pc}$Z7S1vePsf>}iScyWFS14$ z(adm!`LxR1&`|KZ`#L*WGmbdZfTCFC{Fx8B5Fg4Ah{v$>NA-=re(jg9eK2)B`rAyg zeox7@r!czYZg?+qIa73Zl-%8m?(U+yw{UF9;VF!-n23AesZs-PU+fM#=ye*jNx$0; z+B2xZ{lc$UeFenJP!ka*D&$2-0CoV@)Bvx6Cvf!g1^NYsr_69`{ggF9rNfCYlB0kN z4-O#7ArwrsaRtS7XBn$ccJmf%h2g~7FG2%h`Yt@He*&Q+Vqy&s5gGv0hjBGk#W=TN zDiJt48a6_}hIvuJK#GrSjf`p}@%fveW!>T?Z|L~(0fwv`L&Eq?;y3BIv2*8+CG)d6 zfKD>0nG@OB*_5hTfDiyaGqCSn@aa0BY{|0*XgHkuDsXZoIOSBvm>*uKgGY&iKs z+q77_=W0`_wSTd-zu3APT7|JC$$V+_!sz_aJ*jcY?tIUG+5d}OC8=>iZ}wmAzw*PP z-B*%)3zBci(Xb#j0MblN%aZS@(gUV19u4-=+k^%?rQ5Ux^;R0}mTtFMQ17N7f%4wb z5fe)#fAY=2@n!{%wl(8?hi(d*n98UxMI?f810p?$LCG6MYMdcYg^@(6is27AF)A7v$TSd=mrS^fsSz3xBBGIa z%+RRF8E+Ryt2F-jSKxR5tUbjYzY|Elb1Iw9Bw?=|3J`u?i6tOHY38J?##3p(g|&m> z4cusYYer_WCZv*@N2keHHY3NfES7hzt8i@41gHSb@RKYlQF(?H0(%TXZz9QW=wWHou3-TE(BUzzG zt{wsLb*MEWt{;8?w%sprn&rEpxhmk!>nQ|o4$#2>R1DAVr6}))NlG6GBYou#&Ey=R^68mW5=@&tvPfk#~>w+ zYo@7`oK9+{H{eCGU;<}_PeDPKFFQ#?(|VrtUpW0EkFVtE zU-b0QARqDQAS#jQBdy+HbZr1kdCxTcZOO(zgtH~ zywY8d1@$(H(u?vL+L6E-r=9;Kqhok+WP3ntmAm2juSH5L^{)^R*Y}mN`tT%gZ4oJ> z>%8&*87VjAji|VVXVR@$Y3E7}cP~3Jd6%WerwWE&aIaxMy4P?Ix)KU=hT5X{zZ_=@+hp? z*9Up#qnXw7xm0GF;blQ{!mW7#Iqz&-ks1D7rP;BIC;y6m6L4UMg689jeyze7Z~TjJ ziVwhzQvc^dgj9%M8#O7HRaBnwGt7xJF_%{vQYAZv0*{IHpulWN=T++w_}zS3KFZ#J zK8$b5mmta_rSu^=ut*L(Ab|&D+XK@4fcQXpNRBU(!@jc?4q0a+uo!T= twenty_four_hours_ago, - ) - ) - ) - recent_failures = recent_failures_result.scalar() or 0 - - # Recent executions (last 10, ordered by started_at desc) - recent_executions_result = await db.execute( - select(JobExecution).order_by(JobExecution.started_at.desc()).limit(10) - ) - recent_executions = recent_executions_result.scalars().all() - - return DashboardStats( - active_jobs=active_jobs, - total_backups=total_backups, - storage_used_bytes=storage_used_bytes, - recent_failures=recent_failures, - recent_executions=list(recent_executions), - ) diff --git a/backend/app/routers/executions.py b/backend/app/routers/executions.py deleted file mode 100644 index c51dab8..0000000 --- a/backend/app/routers/executions.py +++ /dev/null @@ -1,23 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select -from typing import List -from app.database import get_db -from app.models import JobExecution -from app.schemas import JobExecution as JobExecutionSchema - -router = APIRouter(prefix="/api/executions", tags=["executions"]) - -@router.get("/", response_model=List[JobExecutionSchema]) -async def list_executions(db: AsyncSession = Depends(get_db)): - result = await db.execute(select(JobExecution).order_by(JobExecution.started_at.desc())) - executions = result.scalars().all() - return executions - -@router.get("/{execution_id}", response_model=JobExecutionSchema) -async def get_execution(execution_id: int, db: AsyncSession = Depends(get_db)): - result = await db.execute(select(JobExecution).where(JobExecution.id == execution_id)) - execution = result.scalar_one_or_none() - if not execution: - raise HTTPException(status_code=404, detail="Execution not found") - return execution diff --git a/backend/app/routers/jobs.py b/backend/app/routers/jobs.py deleted file mode 100644 index 3a7b378..0000000 --- a/backend/app/routers/jobs.py +++ /dev/null @@ -1,101 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select -from typing import List -from app.database import get_db, AsyncSessionLocal -from app.models import Job, Schedule -from app.schemas import JobCreate, JobUpdate, Job as JobSchema, ScheduleCreate, Schedule as ScheduleSchema -from backup.engine import BackupEngine - -router = APIRouter(prefix="/api/jobs", tags=["jobs"]) - -@router.get("/", response_model=List[JobSchema]) -async def list_jobs(db: AsyncSession = Depends(get_db)): - result = await db.execute(select(Job)) - jobs = result.scalars().all() - return jobs - -@router.post("/", response_model=JobSchema) -async def create_job(job: JobCreate, db: AsyncSession = Depends(get_db)): - db_job = Job(**job.model_dump()) - db.add(db_job) - await db.commit() - await db.refresh(db_job) - return db_job - -@router.get("/{job_id}", response_model=JobSchema) -async def get_job(job_id: int, db: AsyncSession = Depends(get_db)): - result = await db.execute(select(Job).where(Job.id == job_id)) - job = result.scalar_one_or_none() - if not job: - raise HTTPException(status_code=404, detail="Job not found") - return job - -@router.put("/{job_id}", response_model=JobSchema) -async def update_job( - job_id: int, - job_update: JobUpdate, - db: AsyncSession = Depends(get_db) -): - result = await db.execute(select(Job).where(Job.id == job_id)) - job = result.scalar_one_or_none() - if not job: - raise HTTPException(status_code=404, detail="Job not found") - - update_data = job_update.model_dump(exclude_unset=True) - for field, value in update_data.items(): - setattr(job, field, value) - - await db.commit() - await db.refresh(job) - return job - -@router.delete("/{job_id}") -async def delete_job(job_id: int, db: AsyncSession = Depends(get_db)): - result = await db.execute(select(Job).where(Job.id == job_id)) - job = result.scalar_one_or_none() - if not job: - raise HTTPException(status_code=404, detail="Job not found") - - await db.delete(job) - await db.commit() - return {"message": "Job deleted"} - -@router.post("/{job_id}/run") -async def run_job( - job_id: int, - background_tasks: BackgroundTasks, - db: AsyncSession = Depends(get_db) -): - result = await db.execute(select(Job).where(Job.id == job_id)) - job = result.scalar_one_or_none() - if not job: - raise HTTPException(status_code=404, detail="Job not found") - - # Run in background - async def execute(): - async with AsyncSessionLocal() as session: - engine = BackupEngine(session) - await engine.execute_job(job_id, triggered_by="manual") - - background_tasks.add_task(execute) - return {"message": "Job execution started"} - -@router.post("/{job_id}/schedule", response_model=ScheduleSchema) -async def create_schedule( - job_id: int, - schedule: ScheduleCreate, - db: AsyncSession = Depends(get_db) -): - result = await db.execute(select(Job).where(Job.id == job_id)) - job = result.scalar_one_or_none() - if not job: - raise HTTPException(status_code=404, detail="Job not found") - - schedule_data = schedule.model_dump() - schedule_data["job_id"] = job_id - db_schedule = Schedule(**schedule_data) - db.add(db_schedule) - await db.commit() - await db.refresh(db_schedule) - return ScheduleSchema.model_validate(db_schedule) diff --git a/backend/app/routers/settings.py b/backend/app/routers/settings.py deleted file mode 100644 index f69c846..0000000 --- a/backend/app/routers/settings.py +++ /dev/null @@ -1,43 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select -from typing import List -from app.database import get_db -from app.models import Setting -from app.schemas import Setting as SettingSchema, SettingUpdate - -router = APIRouter(prefix="/api/settings", tags=["settings"]) - -@router.get("/", response_model=List[SettingSchema]) -async def list_settings(db: AsyncSession = Depends(get_db)): - result = await db.execute(select(Setting)) - settings = result.scalars().all() - return settings - -@router.get("/{key}", response_model=SettingSchema) -async def get_setting(key: str, db: AsyncSession = Depends(get_db)): - result = await db.execute(select(Setting).where(Setting.key == key)) - setting = result.scalar_one_or_none() - if not setting: - raise HTTPException(status_code=404, detail="Setting not found") - return setting - -@router.put("/{key}", response_model=SettingSchema) -async def update_setting( - key: str, - setting_update: SettingUpdate, - db: AsyncSession = Depends(get_db) -): - result = await db.execute(select(Setting).where(Setting.key == key)) - setting = result.scalar_one_or_none() - - if not setting: - # Create if not exists - setting = Setting(key=key, value=setting_update.value) - db.add(setting) - else: - setting.value = setting_update.value - - await db.commit() - await db.refresh(setting) - return setting diff --git a/backend/app/routers/sources.py b/backend/app/routers/sources.py deleted file mode 100644 index 3d4fac6..0000000 --- a/backend/app/routers/sources.py +++ /dev/null @@ -1,61 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select -from typing import List -from app.database import get_db -from app.models import Source -from app.schemas import SourceCreate, SourceUpdate, Source as SourceSchema - -router = APIRouter(prefix="/api/sources", tags=["sources"]) - -@router.get("/", response_model=List[SourceSchema]) -async def list_sources(db: AsyncSession = Depends(get_db)): - result = await db.execute(select(Source)) - sources = result.scalars().all() - return sources - -@router.post("/", response_model=SourceSchema) -async def create_source(source: SourceCreate, db: AsyncSession = Depends(get_db)): - db_source = Source(**source.model_dump()) - db.add(db_source) - await db.commit() - await db.refresh(db_source) - return db_source - -@router.get("/{source_id}", response_model=SourceSchema) -async def get_source(source_id: int, db: AsyncSession = Depends(get_db)): - result = await db.execute(select(Source).where(Source.id == source_id)) - source = result.scalar_one_or_none() - if not source: - raise HTTPException(status_code=404, detail="Source not found") - return source - -@router.put("/{source_id}", response_model=SourceSchema) -async def update_source( - source_id: int, - source_update: SourceUpdate, - db: AsyncSession = Depends(get_db) -): - result = await db.execute(select(Source).where(Source.id == source_id)) - source = result.scalar_one_or_none() - if not source: - raise HTTPException(status_code=404, detail="Source not found") - - update_data = source_update.model_dump(exclude_unset=True) - for field, value in update_data.items(): - setattr(source, field, value) - - await db.commit() - await db.refresh(source) - return source - -@router.delete("/{source_id}") -async def delete_source(source_id: int, db: AsyncSession = Depends(get_db)): - result = await db.execute(select(Source).where(Source.id == source_id)) - source = result.scalar_one_or_none() - if not source: - raise HTTPException(status_code=404, detail="Source not found") - - await db.delete(source) - await db.commit() - return {"message": "Source deleted"} diff --git a/backend/app/schemas.py b/backend/app/schemas.py deleted file mode 100644 index a7364b2..0000000 --- a/backend/app/schemas.py +++ /dev/null @@ -1,143 +0,0 @@ -from pydantic import BaseModel, Field, ConfigDict, field_validator -from typing import Optional, List, Dict, Any -from datetime import datetime -import re - -# Source schemas -class SourceBase(BaseModel): - name: str - type: str = Field(..., pattern="^(local|ssh|database)$") - config: Dict[str, Any] = Field(default_factory=dict) - -class SourceCreate(SourceBase): - pass - -class SourceUpdate(BaseModel): - name: Optional[str] = None - config: Optional[Dict[str, Any]] = None - -class Source(SourceBase): - id: int - created_at: datetime - updated_at: datetime - - model_config = ConfigDict(from_attributes=True) - -# Job schemas -class JobBase(BaseModel): - name: str - source_id: int - strategy: str = Field(default="full", pattern="^(full|incremental)$") - destination_path: str - exclude_patterns: List[str] = Field(default_factory=list) - enabled: bool = True - -class JobCreate(JobBase): - pass - -class JobUpdate(BaseModel): - name: Optional[str] = None - strategy: Optional[str] = None - destination_path: Optional[str] = None - exclude_patterns: Optional[List[str]] = None - enabled: Optional[bool] = None - -class Job(JobBase): - id: int - created_at: datetime - updated_at: datetime - - model_config = ConfigDict(from_attributes=True) - -# Schedule schemas -class ScheduleBase(BaseModel): - job_id: int - cron_expression: str - enabled: bool = True - - @field_validator('cron_expression') - @classmethod - def validate_cron(cls, v: str) -> str: - pattern = r'^([0-9*,/-]+)\s+([0-9*,/-]+)\s+([0-9*,/-]+)\s+([0-9*,/-]+)\s+([0-9*,/-]+)$' - if not re.match(pattern, v): - raise ValueError('Invalid cron expression format') - return v - -class ScheduleCreate(ScheduleBase): - pass - -class ScheduleUpdate(BaseModel): - cron_expression: Optional[str] = None - enabled: Optional[bool] = None - -class Schedule(ScheduleBase): - id: int - created_at: datetime - updated_at: datetime - - model_config = ConfigDict(from_attributes=True) - -# Execution schemas -class JobExecutionBase(BaseModel): - job_id: int - status: str = Field(default="pending", pattern="^(pending|running|success|failed|cancelled)$") - triggered_by: str = Field(..., pattern="^(manual|schedule)$") - -class JobExecutionCreate(JobExecutionBase): - pass - -class JobExecution(JobExecutionBase): - id: int - started_at: Optional[datetime] = None - completed_at: Optional[datetime] = None - bytes_processed: int = 0 - bytes_backed_up: int = 0 - error_message: Optional[str] = None - - model_config = ConfigDict(from_attributes=True) - -class JobExecutionUpdate(BaseModel): - status: Optional[str] = Field(None, pattern="^(pending|running|success|failed|cancelled)$") - error_message: Optional[str] = None - -# Backup schemas -class BackupBase(BaseModel): - execution_id: int - storage_path: str - size_bytes: int = 0 - checksum: Optional[str] = None - type: str = Field(..., pattern="^(full|incremental)$") - parent_backup_id: Optional[int] = None - -class BackupCreate(BackupBase): - pass - -class Backup(BackupBase): - id: int - created_at: datetime - - model_config = ConfigDict(from_attributes=True) - -# Settings schemas -class SettingBase(BaseModel): - key: str - value: Optional[str] = None - -class SettingCreate(SettingBase): - pass - -class SettingUpdate(BaseModel): - value: Optional[str] = None - -class Setting(SettingBase): - updated_at: datetime - - model_config = ConfigDict(from_attributes=True) - -# Dashboard schemas -class DashboardStats(BaseModel): - active_jobs: int - total_backups: int - storage_used_bytes: int - recent_failures: int - recent_executions: List[JobExecution] diff --git a/backend/backup/__pycache__/engine.cpython-314.pyc b/backend/backup/__pycache__/engine.cpython-314.pyc deleted file mode 100644 index ceb472caf373307ebb82d9a9fc05318e106df37c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7721 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 SourceAdapter: - adapter_class = ADAPTER_MAP.get(source_type) - if not adapter_class: - raise ValueError(f"Unknown source type: {source_type}") - return adapter_class(config) diff --git a/backend/backup/adapters/__pycache__/__init__.cpython-314.pyc b/backend/backup/adapters/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 797a5420e39cadac9fb600302ba351ee485e3666..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1055 zcmZ8gy>HV%6o3ARlM^63cbmByDO(=Zk2W zGB9+62?hp6kc!lSk^g`tQkQaS)q$y7P={`aJ0~U*Prm2h`?$OJe$T#@h)03SkB9um z9}eIbGXjwY;B1Y84pczr@{r>&m*X*?h6<hGN5(*pjzRMRj_JxZP-Ca;LX{`tV#C5j$T~YJN8$yyjVwD` zHCxz5)De5?T4r--M+=4A&`K_v*jzKcU;_y?SvnCsuTJFZAZXEEfh{JiPl2wp3RN8_ zLWO_y2B1oP6Y!j?b2?X|k-q4Bi4XA5TM`4`eg5{900ZbkDPR#af%Yh#vfV`S{H6@N z)3S+Vxz2i{MkHk8HgZ&v7K;&LB_Vb$3Q0{fe4t$!~C(zRjqv15LVNADnNX#%y$8oWVZNoq#^x!yMo=Q~* z&OziRe>3&5&||N?>wDg}h0ohxr+xE#*$ zXz@k<<{(;2GV)8Tb6<2imKfaQgWNh-v1jPNvUphOs-FQ3ax8 z7qhD`mULq|yGn$bjY+gV{;CL#9yAoi1@FjERsGE-wru`<4Ze_X#v`SF%p zZ8z;Xl%^rZtj8+s2^`0rz{D}6{U>z{H+m9;V!Ne7F|s>*C??(}dtoujca-xOowa}D EUq0>UfdBvi diff --git a/backend/backup/adapters/__pycache__/base.cpython-314.pyc b/backend/backup/adapters/__pycache__/base.cpython-314.pyc deleted file mode 100644 index 300c667ea66eca1e26b98e8658cbcddb817baeda..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3525 zcmcInO^g&p6t3!@`JZKx{o!YEo8LGAc7lKsA;`Ljvj$gfBF31d)6>(l+tAZJR@WGo zgISS;oAIaz6FBa{#65W6s5g(qkeEsg8c*D29Rqmty_%lg?FDv`#7g$fd-Yzue)ZLR zuWAqU6>|i}NBfjZ+ciS|M4>nInCS$-%#(}6q?4pVIjtyM8AVx{R4W?SDmvFIDW0mN zd0Nh^lbK4EXGh6&c985MrZ!AW{ixFQcQkg(b5fTAU3!`xRlCzXZ)Wz8QuaQIB}J&? zZ%l|jyBcw3JG|lY*|1j9gm%u0xX{jc4hNH9P87$Zxxks^F0*;aN>pTPHn$z$jv^=i zp!|u0Af1CC+OVx@!l_BPa#82%5DCcza)g?6nD`m4nF`l05S}ts2uTx}v!3rx2KCUP zuo(Yv0Bi(<@4OAlkNu_;3@D*%qzgFF|Puz zArIfv?_;b&YubEPXpwi-6~#ta^Xi^kv$)rAMZt@#n#WuRc0331sQ{8ty?_h7?uRzu zQw>8OKupy`)&RL^SrU;Nz-Czy`rkb-UN6sv4YzFj?v-+rg_pozRIb|24egy z6v_7ipGH1I2DaVNwk~B+$dN6>ceJ6u@*s3zAz2_(rL@RcR$w>qLpjR=JX${T5W*)d z+mAJyEUWIZi2GjP1|g_7uL`B!3LFd*0Dy=+$FezRUbV&DsB5_lk>mQlWi3!PjKSl1 zpJrQuvV4!oU8O;SUELtp3ER=rB$5tO1V`s5Ac>?atW7#ofz<2g+B75{B%+Fmi1p+5 zFV{^f*6C6Q^|MNsp4QDxa%Ea-GpP0H^4d(iHY>Hc#M*4E%S&A$q05;?o`+H>n0=6% z{i1k2Y%#|j@20|&>){pH?33%kq*xDF2k@sHQ4VfLbh@KV;R6aQf+SK-7}UL)lFCvb zvKNpb+MpjoUY<)gi@OK24Q9pmL~MyHM6b~ZkVo_6A))%tB_!YQTN4XgZdShiOp5Xl z9dz3p$U`w_IvBw``LG{5Vt1HWs&opHzb*w?U;FpV$S`faz=Nt2Lu~3fq@(t&=-UhI(@+GCZiC#2_HYEdbFfm9eoq(tLl5vtO3UuKv(g? zz~G?~$83@0;lS%D|M5rF5w=4t)y=@*AgR~7Wy+H?g(vAfEq+)Y<~&4t1c zY<337#!&AAs|cHY0_ug@;*s0x_s72rB;-Ht{JC{^{DbF${I$1SZs6+`-&aPh)o6|x z?*{&y;We7R+i(N!!ij`z#isWn>m3ZfZ4(ExdB){d&!vSf#c1+?9y8brK2{Ej9RP6( zp4FsOWIIuXmk^VuX&^U-1lusfM;<$YWE{y^B=|IDyOHccg1%S3r|`wgkQ7jH1qi&S z;D%P85X%AJW6 zpKE%-43k|hh0}On%iIz9<$pKah-GnFJ`&~5Mo^S5Lwr4o%*pPv^%Q##M7-f6?D1`t tQhJZ_L)B5)3DUz@6Ix_y0gvfYEnXKNLb!N!k%+kO`<)&nG>cv zNXYvFKl?(|lKP|OXh^c8Ud=GA+%{t(8S61s7BzKdNgZ0uw8XaLL`bl52lOJPeINS6`h?y16fkWn5IlX!;ns9a; zqp&4>un6$rURSpPX{D2}uk*qOzaS(I+)8qqj>0a(LL&(4p7;WLTOOvGX_oA}M?e^! zVV#XdL12m!OGFJL4uVn?13PY?w2n27>SJnSG@*_+rkH*~jhRN{P&9TiovJr=J;Cd0 zGVbsiqqaI*O2fY}r)$U0rVAhrukH@Q_eabP(s_r60_BbLsbQ z<`5F0O^yb3JB=gxDDS~+w{KX8&>V6=aQlHC#2w_2s8KZ16d0>T8!i=~)doR6!C^g} zj2{X1g*=ugrYDEB5lb9VO-s-XRtA$=9(5cn+pr4JUi);j69=4NOlP1*$xsu+mVYQ} zs0z-_6<8Z&#W>On^CdxbRv<(C4wp;51NyxH2`&f9rcd5HIsNg?kMDHO9$yUXoAN)+ zFTDQAwNI{JzIOSRwv@l;i~K!Li*_v39-f<+yST8keX;2HLeBA*A}I)cW1#%5v=y}l z=>Kb`EElF=1wcXqE5v#c5XU0a=tOpq^^i^rWg<$ z(Hd6|1Nkm*9iV3cB!NT*h^$IGm)C{G_IC-Ju9MMb8K+v4NxjYX9eTga)$i=yF8t_iosPQ*CnI z1*G#cLUivSDw3BLdRqDsiG!#r0bn9tfP4<{5xIc4qdId4ad$le%B>_PVb9x0SUMH( z<6AyeQ1cyQ-IKfHZO}e>f{+1k|Av#h`*GJyc_LCj+T5lxhz7ySAbJijFcYG{rV?2P zCU*}JPd|FnrUG1aqfG_oEZ@X}Pj`1=&--YM$Z;&Anwwv(a*LjStaL9a-@o=Y&5m&d>&@-JO$p(4J z&@QP~?wB6ehBY;=nA(_X+47bP-7FY!k%M1Hv&~Ay* zR)M21WTNa0)(lEORj~*qOR-4b>q`FpG)~&U*vZ^DJXkhyk)xiX7g`a zraG2$3a|HG>-~M%(-PRmQhDfMd1yAYSl+Z)a%j1ve0uQa;PSTZx5sA2maA%RpPf1T zyK`Ur<*j+Ih@6}MGMAL>UD-+sN~RzRpyyp}_jOd#`MfHmb#43Ll@S|%fS|xauue$i znr1kkB@a!Qs`2hnPz#-nAP^o3JaXkr0_5)T6@1-0_4%$xb;mgC?O1?{76nMalHiIz zbR@_`{**Ylo|yHoqcg1U6u?(8EVww|LKQJBq(wz=5vdDs`vSPlIGq%GQh*d-+m9VA#IYRntkMiDT<3FJGGOZO zb>Zbe)uX`9DgQrNqd?6 zppLc&d=KigvEJ+jI#05pbv~Q6mrCWac@V19t8~kTodrB zyuz=_YgusjK{e*Cc_5GfHzxCIbpYcW%hw7}->!`C4s(9c2o9;!%uUKvqOb=~u-6 f70LfQDSJltFOvPwNa$;64_tR{eE5psn@{s!+&`*L diff --git a/backend/backup/adapters/base.py b/backend/backup/adapters/base.py deleted file mode 100644 index b48ce08..0000000 --- a/backend/backup/adapters/base.py +++ /dev/null @@ -1,39 +0,0 @@ -from abc import ABC, abstractmethod -from typing import List, Dict, Any, AsyncIterator -from dataclasses import dataclass - -@dataclass -class FileInfo: - path: str - size: int - modified_time: float - is_directory: bool - -class SourceAdapter(ABC): - def __init__(self, config: Dict[str, Any]): - self.config = config - - @abstractmethod - async def connect(self) -> None: - """Establish connection to source.""" - pass - - @abstractmethod - async def disconnect(self) -> None: - """Close connection to source.""" - pass - - @abstractmethod - async def list_files(self, path: str = "") -> List[FileInfo]: - """List files at given path.""" - pass - - @abstractmethod - async def read_file(self, path: str) -> AsyncIterator[bytes]: - """Read file in chunks.""" - pass - - @abstractmethod - async def get_database_dump(self, config: Dict[str, Any]) -> AsyncIterator[bytes]: - """Get database dump. Only implemented for database adapters.""" - pass diff --git a/backend/backup/adapters/database.py b/backend/backup/adapters/database.py deleted file mode 100644 index 3714bd0..0000000 --- a/backend/backup/adapters/database.py +++ /dev/null @@ -1,74 +0,0 @@ -import os -import subprocess -import tempfile -from typing import List, AsyncIterator, Dict, Any -from .base import SourceAdapter, FileInfo - - -class DatabaseAdapter(SourceAdapter): - async def connect(self) -> None: - pass - - async def disconnect(self) -> None: - pass - - async def list_files(self, path: str = "") -> List[FileInfo]: - return [] - - async def read_file(self, path: str) -> AsyncIterator[bytes]: - raise NotImplementedError("Database adapter does not support file reading") - - async def get_database_dump(self, config: Dict[str, Any]) -> AsyncIterator[bytes]: - db_type = config.get("db_type", "postgresql") - host = config.get("host", "localhost") - port = config.get("port", 5432 if db_type == "postgresql" else 3306) - database = config.get("database") - username = config.get("username") - password = config.get("password") - - if not database: - raise ValueError("Database name is required") - - with tempfile.NamedTemporaryFile(suffix=".sql", delete=False) as tmp: - tmp_path = tmp.name - - try: - if db_type == "postgresql": - env = os.environ.copy() - if password: - env["PGPASSWORD"] = password - - cmd = [ - "pg_dump", - "-h", host, - "-p", str(port), - "-U", username or "postgres", - "-f", tmp_path, - database - ] - elif db_type == "mysql": - env = os.environ.copy() - if password: - env["MYSQL_PWD"] = password - - cmd = [ - "mysqldump", - "-h", host, - "-P", str(port), - "-u", username or "root", - "--result-file", tmp_path, - database - ] - else: - raise ValueError(f"Unsupported database type: {db_type}") - - result = subprocess.run(cmd, env=env, capture_output=True, text=True) - if result.returncode != 0: - raise RuntimeError(f"Database dump failed: {result.stderr}") - - with open(tmp_path, "rb") as f: - while chunk := f.read(8192): - yield chunk - finally: - if os.path.exists(tmp_path): - os.unlink(tmp_path) diff --git a/backend/backup/adapters/local.py b/backend/backup/adapters/local.py deleted file mode 100644 index 5a2f500..0000000 --- a/backend/backup/adapters/local.py +++ /dev/null @@ -1,47 +0,0 @@ -import os -import aiofiles -from pathlib import Path -from typing import List, AsyncIterator, Dict, Any -from .base import SourceAdapter, FileInfo - -class LocalAdapter(SourceAdapter): - async def connect(self) -> None: - base_path = self.config.get("path", ".") - if not os.path.exists(base_path): - raise FileNotFoundError(f"Path not found: {base_path}") - - async def disconnect(self) -> None: - pass - - async def list_files(self, path: str = "") -> List[FileInfo]: - base_path = Path(self.config.get("path", ".")) - target_path = base_path / path if path else base_path - - files = [] - exclude_patterns = self.config.get("exclude", []) - - for item in target_path.iterdir(): - # Check exclude patterns - if any(item.match(pattern) for pattern in exclude_patterns): - continue - - stat = item.stat() - files.append(FileInfo( - path=str(item.relative_to(base_path)), - size=stat.st_size, - modified_time=stat.st_mtime, - is_directory=item.is_dir() - )) - - return files - - async def read_file(self, path: str) -> AsyncIterator[bytes]: - base_path = Path(self.config.get("path", ".")) - file_path = base_path / path - - async with aiofiles.open(file_path, "rb") as f: - while chunk := await f.read(8192): - yield chunk - - async def get_database_dump(self, config: Dict[str, Any]) -> AsyncIterator[bytes]: - raise NotImplementedError("Local adapter does not support database dumps") diff --git a/backend/backup/adapters/ssh.py b/backend/backup/adapters/ssh.py deleted file mode 100644 index 11ba55e..0000000 --- a/backend/backup/adapters/ssh.py +++ /dev/null @@ -1,92 +0,0 @@ -import os -import tempfile -from pathlib import Path -from typing import List, AsyncIterator, Dict, Any -import paramiko -from .base import SourceAdapter, FileInfo - - -class SSHAdapter(SourceAdapter): - def __init__(self, config: Dict[str, Any]): - super().__init__(config) - self.client = None - self.sftp = None - - async def connect(self) -> None: - self.client = paramiko.SSHClient() - self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - - host = self.config.get("host", "localhost") - port = self.config.get("port", 22) - username = self.config.get("username") - password = self.config.get("password") - key_path = self.config.get("key_path") - - connect_kwargs = { - "hostname": host, - "port": port, - "username": username, - } - - if password: - connect_kwargs["password"] = password - elif key_path and os.path.exists(key_path): - connect_kwargs["key_filename"] = key_path - - self.client.connect(**connect_kwargs) - self.sftp = self.client.open_sftp() - - async def disconnect(self) -> None: - if self.sftp: - self.sftp.close() - self.sftp = None - if self.client: - self.client.close() - self.client = None - - async def list_files(self, path: str = "") -> List[FileInfo]: - remote_path = self.config.get("path", ".") - target_path = f"{remote_path}/{path}" if path else remote_path - - files = [] - exclude_patterns = self.config.get("exclude", []) - - try: - for entry in self.sftp.listdir_attr(target_path): - entry_path = f"{target_path}/{entry.filename}" - rel_path = entry_path.replace(remote_path + "/", "", 1) if remote_path != "." else entry_path - - if any(pattern in rel_path for pattern in exclude_patterns): - continue - - is_dir = entry.st_mode & 0o40000 == 0o40000 if hasattr(entry, 'st_mode') else False - - files.append(FileInfo( - path=rel_path, - size=entry.st_size, - modified_time=entry.st_mtime, - is_directory=is_dir - )) - except IOError: - pass - - return files - - async def read_file(self, path: str) -> AsyncIterator[bytes]: - remote_path = self.config.get("path", ".") - file_path = f"{remote_path}/{path}" if not path.startswith("/") else path - - with tempfile.NamedTemporaryFile(delete=False) as tmp: - tmp_path = tmp.name - - try: - self.sftp.get(file_path, tmp_path) - with open(tmp_path, "rb") as f: - while chunk := f.read(8192): - yield chunk - finally: - if os.path.exists(tmp_path): - os.unlink(tmp_path) - - async def get_database_dump(self, config: Dict[str, Any]) -> AsyncIterator[bytes]: - raise NotImplementedError("SSH adapter does not support database dumps directly") diff --git a/backend/backup/engine.py b/backend/backup/engine.py deleted file mode 100644 index 922c91c..0000000 --- a/backend/backup/engine.py +++ /dev/null @@ -1,138 +0,0 @@ -import os -import hashlib -import shutil -from datetime import datetime, timezone -from pathlib import Path -from typing import Optional -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select -from app.models import Job, JobExecution, Backup -from backup.adapters import get_adapter -from backup.retention import RetentionPolicy - -class BackupEngine: - def __init__(self, db: AsyncSession): - self.db = db - - async def execute_job(self, job_id: int, triggered_by: str = "manual") -> JobExecution: - # Create execution record - execution = JobExecution( - job_id=job_id, - status="pending", - triggered_by=triggered_by - ) - self.db.add(execution) - await self.db.commit() - await self.db.refresh(execution) - - try: - # Load job with source - result = await self.db.execute( - select(Job).where(Job.id == job_id) - ) - job = result.scalar_one() - - # Update status to running - execution.status = "running" - execution.started_at = datetime.now(timezone.utc) - await self.db.commit() - - # Determine strategy - strategy = job.strategy - parent_backup_id = None - - if strategy == "incremental": - # Find last successful full backup - result = await self.db.execute( - select(Backup) - .join(JobExecution) - .where( - JobExecution.job_id == job_id, - JobExecution.status == "success", - Backup.type == "full" - ) - .order_by(Backup.created_at.desc()) - ) - last_full = result.scalar_one_or_none() - - if last_full: - parent_backup_id = last_full.id - else: - # No full backup exists, do full instead - strategy = "full" - - # Create backup directory - timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d_%H%M%S") - backup_dir = Path(job.destination_path) / str(job_id) / f"{timestamp}_{strategy}" - backup_dir.mkdir(parents=True, exist_ok=True) - - # Get adapter and connect - adapter = get_adapter(job.source.type, job.source.config) - await adapter.connect() - - try: - # Copy files - total_processed = 0 - total_backed_up = 0 - - source_path = Path(job.source.config.get("path", ".")) - - for item in source_path.rglob("*"): - if item.is_file(): - rel_path = item.relative_to(source_path) - dest_path = backup_dir / "data" / rel_path - dest_path.parent.mkdir(parents=True, exist_ok=True) - - # Copy file - shutil.copy2(item, dest_path) - - size = item.stat().st_size - total_processed += size - total_backed_up += size - - # Calculate checksum - checksum = await self._calculate_checksum(backup_dir) - - # Create backup record - backup = Backup( - execution_id=execution.id, - storage_path=str(backup_dir), - size_bytes=total_backed_up, - checksum=checksum, - type=strategy, - parent_backup_id=parent_backup_id - ) - self.db.add(backup) - - # Update execution - execution.status = "success" - execution.completed_at = datetime.now(timezone.utc) - execution.bytes_processed = total_processed - execution.bytes_backed_up = total_backed_up - - # Apply retention policy - retention = RetentionPolicy(self.db) - keep_count = getattr(job, 'retention_count', None) - keep_days = getattr(job, 'retention_days', None) - if keep_count or keep_days: - await retention.apply_retention_for_job(job_id, keep_count, keep_days) - - finally: - await adapter.disconnect() - - except Exception as e: - execution.status = "failed" - execution.completed_at = datetime.now(timezone.utc) - execution.error_message = str(e) - - await self.db.commit() - return execution - - async def _calculate_checksum(self, path: Path) -> str: - hasher = hashlib.sha256() - for item in sorted(path.rglob("*")): - if item.is_file(): - with open(item, "rb") as f: - while chunk := f.read(8192): - hasher.update(chunk) - return hasher.hexdigest() diff --git a/backend/backup/retention.py b/backend/backup/retention.py deleted file mode 100644 index efb421d..0000000 --- a/backend/backup/retention.py +++ /dev/null @@ -1,90 +0,0 @@ -import shutil -from datetime import datetime, timezone, timedelta -from pathlib import Path -from typing import List, Optional -from sqlalchemy import select - -from app.models import Backup - - -class RetentionPolicy: - def __init__(self, db): - self.db = db - - async def apply_retention_for_job( - self, - job_id: int, - keep_count: Optional[int] = None, - keep_days: Optional[int] = None - ) -> List[Backup]: - """ - Apply retention policy for a job's backups. - - Args: - job_id: The job ID to apply retention for - keep_count: Maximum number of backups to keep (oldest deleted first) - keep_days: Delete backups older than this many days - - Returns: - List of deleted backups - """ - deleted_backups = [] - - result = await self.db.execute( - select(Backup) - .where(Backup.execution.has(job_id=job_id)) - .order_by(Backup.created_at.asc()) - ) - backups = result.scalars().all() - - if not backups: - return deleted_backups - - backups_to_delete = set() - - if keep_count is not None and len(backups) > keep_count: - backups_to_delete.update(backups[:-keep_count]) - - if keep_days is not None: - cutoff_date = datetime.now(timezone.utc) - timedelta(days=keep_days) - for backup in backups: - if backup.created_at < cutoff_date: - backups_to_delete.add(backup) - - for backup in list(backups_to_delete): - await self._delete_backup(backup) - deleted_backups.append(backup) - - await self.db.commit() - return deleted_backups - - async def _delete_backup(self, backup: Backup): - """Delete a backup and its storage.""" - try: - storage_path = Path(backup.storage_path) - if storage_path.exists(): - shutil.rmtree(storage_path) - except Exception: - pass - - await self.db.delete(backup) - - async def cleanup_orphaned_backups(self) -> int: - """ - Remove backup records whose storage no longer exists. - - Returns: - Number of orphaned backups removed - """ - result = await self.db.execute(select(Backup)) - backups = result.scalars().all() - - removed_count = 0 - for backup in backups: - storage_path = Path(backup.storage_path) - if not storage_path.exists(): - await self.db.delete(backup) - removed_count += 1 - - await self.db.commit() - return removed_count diff --git a/backend/backup/scheduler.py b/backend/backup/scheduler.py deleted file mode 100644 index 43d7924..0000000 --- a/backend/backup/scheduler.py +++ /dev/null @@ -1,81 +0,0 @@ -from apscheduler.schedulers.asyncio import AsyncIOScheduler -from apscheduler.triggers.cron import CronTrigger -from sqlalchemy import select -from typing import Optional -import logging - -from app.database import AsyncSessionLocal -from app.models import Schedule -from backup.engine import BackupEngine - -logger = logging.getLogger(__name__) - - -class BackupScheduler: - def __init__(self): - self.scheduler = AsyncIOScheduler() - self._job_map = {} - - def start(self): - """Start the scheduler.""" - self.scheduler.start() - logger.info("Backup scheduler started") - - def shutdown(self): - """Shutdown the scheduler.""" - self.scheduler.shutdown() - logger.info("Backup scheduler shutdown") - - async def sync_schedules(self): - """Sync all enabled schedules from database.""" - async with AsyncSessionLocal() as db: - result = await db.execute( - select(Schedule).where(Schedule.enabled == True) - ) - schedules = result.scalars().all() - - # Clear existing jobs - for schedule_id, job_id in list(self._job_map.items()): - self.scheduler.remove_job(job_id) - del self._job_map[schedule_id] - - # Add new jobs - for schedule in schedules: - await self._add_schedule_job(schedule) - - async def _add_schedule_job(self, schedule: Schedule): - """Add a single schedule job to the scheduler.""" - try: - trigger = CronTrigger.from_crontab(schedule.cron_expression) - job = self.scheduler.add_job( - self._run_backup_job, - trigger=trigger, - args=[schedule.job_id], - id=f"backup_job_{schedule.job_id}", - replace_existing=True - ) - self._job_map[schedule.id] = job.id - logger.info(f"Scheduled backup job {schedule.job_id} with cron: {schedule.cron_expression}") - except Exception as e: - logger.error(f"Failed to schedule job {schedule.job_id}: {e}") - - async def _run_backup_job(self, job_id: int): - """Execute a backup job.""" - logger.info(f"Running scheduled backup job {job_id}") - async with AsyncSessionLocal() as db: - engine = BackupEngine(db) - await engine.execute_job(job_id, triggered_by="schedule") - - async def add_schedule(self, schedule: Schedule): - """Add a new schedule to the scheduler.""" - await self._add_schedule_job(schedule) - - def remove_schedule(self, schedule_id: int): - """Remove a schedule from the scheduler.""" - if schedule_id in self._job_map: - self.scheduler.remove_job(self._job_map[schedule_id]) - del self._job_map[schedule_id] - - -# Global scheduler instance -backup_scheduler = BackupScheduler() diff --git a/backend/backup_tool.db b/backend/backup_tool.db deleted file mode 100644 index c7037e1184bd49c2a0319233415d4522f266673d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53248 zcmeI(U2ob}7zc1WBsh>4I!%>TMOBV^6{eK}At5kL8cp3cYf4%|szkf69DG8(0H%If zliej%Kf>;JIcZ;EA7FReci7EtDowlDChcP980_E(Na>WINPnb&eUAMc`}aJ@U|HFI zzG*m=9GF&9bI7uALl8yb3qk}zklEiH`}1EjY+=%0VV`2p@}NaoNPYM7T;da9Hu_FT zWak_6|IB?f`_s&iGwZQmV_!+XNIL7l3jz>;00ba#nF8*tB(E=sC%252PLGTubziF; zx@}u+YZh%e-Bs1lyX|x96{S>F$VT~rvP*_+PqxaWyAN49A1rCiNb+ZF*x31D=@G5D zj$yXWhMXNVWbd}2;d(ogZ(?ytURV&{8jiN#pxtBYe#~@ZS4;Odm9yiG#|)jFVpVyh zR7iQNO3FK%o1c<+tT%Gs4Q$7>v^r&c&P(#5R9Syq3P-SwZ>hR}>`>d^pU17eq_soa zZTh?LHs`oa{q@j6oYxEmvZ&@zU1h&q9nz!_HcNW zuNr!|F@8S%;qLA}JpbViveb!|JZ(=)axy8tUGNGZyj6HD7Al8uA6Xh$6`g_nG=y?x zJDTI#ebdOgtUjr#nay^C_UZ77Mr~VWjoLO1>XWCk)5Wb>>fN?~B)p4SmT9R?whJpu zQdM@Vyw$OcdYxKq>;2=t2X|4ybWS;xYSz>#ka+SylH}z@@kH|TY_kH_-3GP0=?d1T zyW%_LjpsW(mz7|Hp8N)FXQ`c!O7h~O_~y_{ZLmSGAcYbdG=EqwYZkjK(WAD-FxeHV zuN!Ugi;Pb0Z*6TVr82j!^`DY%TmO)mIvjp`Dk|T*DZcXEc}`T@bgdfgOe{OvXFBz4 z(w#>vk-=Wc)7?o)zIju8`>{WPY#w(iWZTL9z$wzwn*A?k!7zT&o|`@vL#p?$QJvb3 z(bBwci-Ff$dQ@w;x_3c#sMWH`m)l!jS}r|P!I_0-NvGAHC@1j=NxpqsJgIjIm^$o? z>TG&p(LaRL`cft9R2Y zcQYB1O&4>k#cVoNNawSwg|bO3^EG*`+2p`z{} zfB*y_009U<00Izz00ba#-2^5hF)_;Druf?=e~a+9iAW?C!~g$Zw{+roApijgKmY;| zfB*y_009U<00N^6;Q#+eH^UwffB*y_009U<00Izz00bZaf$JuK`~T~fF&r-hAOHaf UKmY;|fB*y_009U=61.0", "wheel"] +requires = ["setuptools==80.9.0", "wheel==0.46.3"] build-backend = "setuptools.build_meta" [project] name = "backup-tool" -version = "0.1.0" -description = "Web-based backup management tool for small teams and SMBs" -readme = "README.md" -license = {text = "MIT"} -requires-python = ">=3.11" -authors = [ - {name = "Backup Tool Team"} -] -keywords = ["backup", "restore", "scheduler", "web"] -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: System Administrators", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Topic :: System :: Archiving :: Backup", -] +version = "2.0.0.dev0" +description = "Self-hosted, integrity-first backup appliance" +license = "MIT" +requires-python = ">=3.12,<3.15" +authors = [{ name = "Backup Tool Team" }] dependencies = [ - "fastapi>=0.115.0", - "uvicorn[standard]>=0.34.0", - "sqlalchemy[asyncio]>=2.0.0", - "aiosqlite>=0.21.0", - "alembic>=1.15.0", - "pydantic>=2.13.0", - "pydantic-settings>=2.9.0", - "apscheduler>=3.11.0", - "paramiko>=3.5.0", - "aiofiles>=23.2.0", - "httpx>=0.28.0", + "aiofiles==25.1.0", + "aiosqlite==0.22.1", + "alembic==1.18.5", + "apscheduler==3.11.3", + "fastapi==0.136.1", + "httpx==0.28.1", + "paramiko==5.0.0", + "pydantic==2.13.4", + "pydantic-settings==2.14.2", + "sqlalchemy[asyncio]==2.0.49", + "uvicorn[standard]==0.51.0", ] [project.optional-dependencies] dev = [ - "pytest>=8.3.0", - "pytest-asyncio>=0.26.0", -] -prod = [ - "gunicorn>=23.0.0", + "jsonschema==4.26.0", + "mypy==2.3.0", + "pytest==9.0.3", + "pytest-asyncio==1.3.0", + "ruff==0.16.0", ] +prod = ["gunicorn==23.0.0"] [project.scripts] -backup-tool = "app.main:main" +backup-tool = "backup_tool.cli:main" -[project.urls] -Homepage = "https://github.com/backup-tool/backup-tool" -Documentation = "https://github.com/backup-tool/backup-tool#readme" -Repository = "https://github.com/backup-tool/backup-tool.git" +[tool.setuptools] +package-dir = { "" = "src" } [tool.setuptools.packages.find] -where = ["."] -include = ["app*", "backup*", "alembic*"] +where = ["src"] +include = ["backup_tool*"] + +[tool.setuptools.package-data] +backup_tool = ["py.typed"] [tool.pytest.ini_options] asyncio_mode = "auto" -testpaths = ["tests"] -pythonpath = [".", "app", "backup"] -[tool.setuptools.package-data] -alembic = ["*.ini", "*.py", "*.mako"] +[tool.ruff] +target-version = "py312" +line-length = 100 + +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "B", "SIM"] + +[tool.mypy] +python_version = "3.12" +strict = true +packages = ["backup_tool"] +mypy_path = "src" diff --git a/backend/src/backup_tool/__init__.py b/backend/src/backup_tool/__init__.py new file mode 100644 index 0000000..0fd3dc5 --- /dev/null +++ b/backend/src/backup_tool/__init__.py @@ -0,0 +1,3 @@ +"""Backup Tool v2 package.""" + +__version__ = "2.0.0.dev0" diff --git a/backend/src/backup_tool/cli.py b/backend/src/backup_tool/cli.py new file mode 100644 index 0000000..343636b --- /dev/null +++ b/backend/src/backup_tool/cli.py @@ -0,0 +1,23 @@ +"""Minimal v2 command entry point; runtime roles arrive in M1.""" + +from __future__ import annotations + +import argparse +from collections.abc import Sequence + +from backup_tool import __version__ + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="backup-tool") + parser.add_argument("--version", action="version", version=__version__) + return parser + + +def main(argv: Sequence[str] | None = None) -> int: + build_parser().parse_args(argv) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/backup_tool.db b/backend/src/backup_tool/py.typed similarity index 100% rename from backup_tool.db rename to backend/src/backup_tool/py.typed diff --git a/backend/tests/__pycache__/conftest.cpython-314-pytest-9.0.3.pyc b/backend/tests/__pycache__/conftest.cpython-314-pytest-9.0.3.pyc deleted file mode 100644 index cc1bf02c6fcb95e0cf3e84ccc626a33974a0a241..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3716 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 T!``Y~IN;OVM^I`HZT5cyC7Vr* diff --git a/backend/tests/__pycache__/test_jobs.cpython-314-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_jobs.cpython-314-pytest-9.0.3.pyc deleted file mode 100644 index 65978cf1e5c7f494bd3ed6b2b8857d7c79d32b7f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23109 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 OcJ;0W;z?6bzyAjYJ$^X= diff --git a/backend/tests/__pycache__/test_sources.cpython-314-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_sources.cpython-314-pytest-9.0.3.pyc deleted file mode 100644 index 40af9a660cc2f4996dde8f1d48391e33d5b3a6b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14411 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 0 - assert execution.bytes_backed_up > 0 - assert execution.triggered_by == "manual" - - # Refresh test_job to load executions relationship - await db.refresh(test_job, ["executions"]) - - # Verify backup was created - assert len(test_job.executions) == 1 - - # Refresh execution to load backups relationship - await db.refresh(test_job.executions[0], ["backups"]) - backup = test_job.executions[0].backups[0] - assert backup.type == "full" - assert backup.checksum is not None - assert os.path.exists(backup.storage_path) - -@pytest.mark.asyncio -async def test_execute_incremental_without_full(db: AsyncSession, test_job): - # Set job to incremental but no full backup exists - test_job.strategy = "incremental" - await db.commit() - - engine = BackupEngine(db) - execution = await engine.execute_job(test_job.id) - - # Should fall back to full backup - assert execution.status == "success" - - # Refresh execution to load backups relationship - await db.refresh(execution, ["backups"]) - backup = execution.backups[0] - assert backup.type == "full" - assert backup.parent_backup_id is None diff --git a/backend/tests/test_jobs.py b/backend/tests/test_jobs.py deleted file mode 100644 index aee96e1..0000000 --- a/backend/tests/test_jobs.py +++ /dev/null @@ -1,205 +0,0 @@ -import pytest - -@pytest.mark.asyncio -async def test_create_job(client): - # Create a source first (job requires source_id) - source_resp = await client.post("/api/sources/", json={ - "name": "Test Source", - "type": "local", - "config": {"path": "/tmp/test"} - }) - assert source_resp.status_code == 200 - source_id = source_resp.json()["id"] - - response = await client.post("/api/jobs/", json={ - "name": "Test Job", - "source_id": source_id, - "strategy": "full", - "destination_path": "/tmp/backups", - "exclude_patterns": [], - "enabled": True - }) - assert response.status_code == 200 - data = response.json() - assert data["name"] == "Test Job" - assert data["source_id"] == source_id - assert data["strategy"] == "full" - assert "id" in data - -@pytest.mark.asyncio -async def test_list_jobs(client): - # Create source and job - source_resp = await client.post("/api/sources/", json={ - "name": "Test Source", - "type": "local", - "config": {"path": "/tmp/test"} - }) - source_id = source_resp.json()["id"] - - await client.post("/api/jobs/", json={ - "name": "Test Job", - "source_id": source_id, - "strategy": "full", - "destination_path": "/tmp/backups", - "exclude_patterns": [], - "enabled": True - }) - - response = await client.get("/api/jobs/") - assert response.status_code == 200 - data = response.json() - assert len(data) >= 1 - -@pytest.mark.asyncio -async def test_get_job(client): - source_resp = await client.post("/api/sources/", json={ - "name": "Test Source", - "type": "local", - "config": {"path": "/tmp/test"} - }) - source_id = source_resp.json()["id"] - - job_resp = await client.post("/api/jobs/", json={ - "name": "Test Job", - "source_id": source_id, - "strategy": "full", - "destination_path": "/tmp/backups", - "exclude_patterns": [], - "enabled": True - }) - job_id = job_resp.json()["id"] - - response = await client.get(f"/api/jobs/{job_id}") - assert response.status_code == 200 - assert response.json()["id"] == job_id - -@pytest.mark.asyncio -async def test_delete_job(client): - source_resp = await client.post("/api/sources/", json={ - "name": "Test Source", - "type": "local", - "config": {"path": "/tmp/test"} - }) - source_id = source_resp.json()["id"] - - job_resp = await client.post("/api/jobs/", json={ - "name": "Delete Me", - "source_id": source_id, - "strategy": "full", - "destination_path": "/tmp/backups", - "exclude_patterns": [], - "enabled": True - }) - job_id = job_resp.json()["id"] - - response = await client.delete(f"/api/jobs/{job_id}") - assert response.status_code == 200 - - # Verify deletion - get_resp = await client.get(f"/api/jobs/{job_id}") - assert get_resp.status_code == 404 - -@pytest.mark.asyncio -async def test_run_job(client): - # Create source - source_resp = await client.post("/api/sources/", json={ - "name": "Test Source", - "type": "local", - "config": {"path": "/tmp/test"} - }) - source_id = source_resp.json()["id"] - - # Create job - job_resp = await client.post("/api/jobs/", json={ - "name": "Test Job", - "source_id": source_id, - "strategy": "full", - "destination_path": "/tmp/backups", - "exclude_patterns": [], - "enabled": True - }) - job_id = job_resp.json()["id"] - - response = await client.post(f"/api/jobs/{job_id}/run") - assert response.status_code == 200 - assert response.json()["message"] == "Job execution started" - -@pytest.mark.asyncio -async def test_run_job_not_found(client): - response = await client.post("/api/jobs/999/run") - assert response.status_code == 404 - -@pytest.mark.asyncio -async def test_update_job(client): - # Create source - source_resp = await client.post("/api/sources/", json={ - "name": "Test Source", - "type": "local", - "config": {"path": "/tmp/test"} - }) - source_id = source_resp.json()["id"] - - # Create job - job_resp = await client.post("/api/jobs/", json={ - "name": "Original Name", - "source_id": source_id, - "strategy": "full", - "destination_path": "/tmp/backups", - "exclude_patterns": [], - "enabled": True - }) - job_id = job_resp.json()["id"] - - response = await client.put(f"/api/jobs/{job_id}", json={ - "name": "Updated Name" - }) - assert response.status_code == 200 - assert response.json()["name"] == "Updated Name" - assert response.json()["strategy"] == "full" # Unchanged - -@pytest.mark.asyncio -async def test_create_schedule(client): - # Create source - source_resp = await client.post("/api/sources/", json={ - "name": "Test Source", - "type": "local", - "config": {"path": "/tmp/test"} - }) - source_id = source_resp.json()["id"] - - # Create job - job_resp = await client.post("/api/jobs/", json={ - "name": "Test Job", - "source_id": source_id, - "strategy": "full", - "destination_path": "/tmp/backups", - "exclude_patterns": [], - "enabled": True - }) - job_id = job_resp.json()["id"] - - response = await client.post(f"/api/jobs/{job_id}/schedule", json={ - "job_id": job_id, - "cron_expression": "0 0 * * *", - "enabled": True - }) - assert response.status_code == 200 - data = response.json() - assert data["job_id"] == job_id - assert data["cron_expression"] == "0 0 * * *" - assert "id" in data - -@pytest.mark.asyncio -async def test_get_job_not_found(client): - response = await client.get("/api/jobs/99999") - assert response.status_code == 404 - -@pytest.mark.asyncio -async def test_update_job_not_found(client): - response = await client.put("/api/jobs/99999", json={"name": "Test"}) - assert response.status_code == 404 - -@pytest.mark.asyncio -async def test_delete_job_not_found(client): - response = await client.delete("/api/jobs/99999") - assert response.status_code == 404 diff --git a/backend/tests/test_sources.py b/backend/tests/test_sources.py deleted file mode 100644 index 14ff2ae..0000000 --- a/backend/tests/test_sources.py +++ /dev/null @@ -1,88 +0,0 @@ -import pytest - -@pytest.mark.asyncio -async def test_create_source(client): - response = await client.post("/api/sources/", json={ - "name": "Test Source", - "type": "local", - "config": {"path": "/tmp/test"} - }) - assert response.status_code == 200 - data = response.json() - assert data["name"] == "Test Source" - assert data["type"] == "local" - assert "id" in data - -@pytest.mark.asyncio -async def test_list_sources(client): - # Create source first - await client.post("/api/sources/", json={ - "name": "Test Source", - "type": "local", - "config": {"path": "/tmp/test"} - }) - - response = await client.get("/api/sources/") - assert response.status_code == 200 - data = response.json() - assert len(data) >= 1 - -@pytest.mark.asyncio -async def test_get_source(client): - create_resp = await client.post("/api/sources/", json={ - "name": "Test Source", - "type": "local", - "config": {"path": "/tmp/test"} - }) - source_id = create_resp.json()["id"] - - response = await client.get(f"/api/sources/{source_id}") - assert response.status_code == 200 - assert response.json()["id"] == source_id - -@pytest.mark.asyncio -async def test_delete_source(client): - create_resp = await client.post("/api/sources/", json={ - "name": "Delete Me", - "type": "local", - "config": {"path": "/tmp/test"} - }) - source_id = create_resp.json()["id"] - - response = await client.delete(f"/api/sources/{source_id}") - assert response.status_code == 200 - - # Verify deletion - get_resp = await client.get(f"/api/sources/{source_id}") - assert get_resp.status_code == 404 - -@pytest.mark.asyncio -async def test_update_source(client): - create_resp = await client.post("/api/sources/", json={ - "name": "Original Name", - "type": "local", - "config": {"path": "/tmp/test"} - }) - source_id = create_resp.json()["id"] - - response = await client.put(f"/api/sources/{source_id}", json={ - "name": "Updated Name" - }) - assert response.status_code == 200 - assert response.json()["name"] == "Updated Name" - assert response.json()["type"] == "local" # Unchanged - -@pytest.mark.asyncio -async def test_get_source_not_found(client): - response = await client.get("/api/sources/99999") - assert response.status_code == 404 - -@pytest.mark.asyncio -async def test_update_source_not_found(client): - response = await client.put("/api/sources/99999", json={"name": "Test"}) - assert response.status_code == 404 - -@pytest.mark.asyncio -async def test_delete_source_not_found(client): - response = await client.delete("/api/sources/99999") - assert response.status_code == 404 diff --git a/contracts/repository/v1/capabilities-v2.0.json b/contracts/repository/v1/capabilities-v2.0.json new file mode 100644 index 0000000..6dff906 --- /dev/null +++ b/contracts/repository/v1/capabilities-v2.0.json @@ -0,0 +1,14 @@ +{ + "api_version": "v2", + "sources": ["local", "ssh"], + "repositories": ["local"], + "features": { + "email": true, + "encryption": true, + "mysql": false, + "postgresql": false, + "restore": true, + "tar_download": false, + "webhook": true + } +} diff --git a/contracts/repository/v1/error-codes.json b/contracts/repository/v1/error-codes.json new file mode 100644 index 0000000..0a2b8cb --- /dev/null +++ b/contracts/repository/v1/error-codes.json @@ -0,0 +1,22 @@ +[ + "authentication_failed", + "baseline_missing", + "cancelled_by_operator", + "capacity_exhausted", + "conflict_active_execution", + "corrupt_blob", + "corrupt_manifest", + "deletion_failed", + "forbidden", + "host_key_changed", + "host_key_unknown", + "invalid_configuration", + "not_found", + "permission_denied", + "source_changed", + "source_empty", + "timeout", + "transient_io", + "unsupported_entry", + "worker_lost" +] diff --git a/contracts/repository/v1/execution-transitions.json b/contracts/repository/v1/execution-transitions.json new file mode 100644 index 0000000..98e0770 --- /dev/null +++ b/contracts/repository/v1/execution-transitions.json @@ -0,0 +1,10 @@ +{ + "queued": ["cancelled", "preparing"], + "preparing": ["cancelling", "failed", "running"], + "running": ["cancelling", "failed", "verifying"], + "verifying": ["committed", "failed"], + "cancelling": ["cancelled", "failed"], + "committed": [], + "failed": [], + "cancelled": [] +} diff --git a/contracts/repository/v1/fault-points.json b/contracts/repository/v1/fault-points.json new file mode 100644 index 0000000..f865e1a --- /dev/null +++ b/contracts/repository/v1/fault-points.json @@ -0,0 +1,14 @@ +[ + "blob.before_write", + "blob.after_write", + "blob.after_fsync", + "blob.before_rename", + "manifest.before_write", + "manifest.after_fsync", + "manifest.before_publish", + "metadata.before_commit", + "metadata.after_commit", + "restore.before_write", + "restore.after_fsync", + "restore.before_replace" +] diff --git a/contracts/repository/v1/fixtures/invalid-manifest.json b/contracts/repository/v1/fixtures/invalid-manifest.json new file mode 100644 index 0000000..a114da6 --- /dev/null +++ b/contracts/repository/v1/fixtures/invalid-manifest.json @@ -0,0 +1 @@ +{"aggregates":{"entry_count":-1,"logical_bytes":-5,"stored_bytes":0},"backup_id":"bad","created_at":"not-a-time","effective_mode":"full","encryption_key_id":null,"entries":[],"exclusion_policy":{"matcher":"glob","patterns":[],"version":0},"execution_id":"bad","format_version":2,"job_id":"bad","manifest_digest":"short","repository_id":"bad","requested_mode":"full","source_consistency":{},"source_id":"bad"} diff --git a/contracts/repository/v1/fixtures/invalid-repository.json b/contracts/repository/v1/fixtures/invalid-repository.json new file mode 100644 index 0000000..42df253 --- /dev/null +++ b/contracts/repository/v1/fixtures/invalid-repository.json @@ -0,0 +1 @@ +{"compression":"none","created_at":"2026-07-27T00:00:00Z","digest_algorithm":"md5","encryption":{"key_id":null,"mode":"none"},"format_version":2,"repository_id":"not-a-uuid"} diff --git a/contracts/repository/v1/fixtures/valid-manifest.json b/contracts/repository/v1/fixtures/valid-manifest.json new file mode 100644 index 0000000..f9b7da6 --- /dev/null +++ b/contracts/repository/v1/fixtures/valid-manifest.json @@ -0,0 +1 @@ +{"aggregates":{"entry_count":2,"logical_bytes":5,"stored_bytes":5},"backup_id":"0198c57f-0000-7000-8000-000000000006","created_at":"2026-07-27T00:00:00Z","effective_mode":"full","encryption_key_id":null,"entries":[{"blob_digest":null,"link_target":null,"metadata_support":["mode","mtime_ns"],"mode":493,"mtime_ns":0,"path":"data","size":0,"type":"directory"},{"blob_digest":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","link_target":null,"metadata_support":["mode","mtime_ns"],"mode":420,"mtime_ns":0,"path":"data/hello.txt","size":5,"type":"file"}],"exclusion_policy":{"matcher":"gitignore","patterns":[],"version":1},"execution_id":"0198c57f-0000-7000-8000-000000000005","format_version":1,"job_id":"0198c57f-0000-7000-8000-000000000004","manifest_digest":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","repository_id":"0198c57f-0000-7000-8000-000000000001","requested_mode":"full","source_consistency":{"adapter":"local","captured_at":"2026-07-27T00:00:00Z"},"source_id":"0198c57f-0000-7000-8000-000000000003"} diff --git a/contracts/repository/v1/fixtures/valid-repository.json b/contracts/repository/v1/fixtures/valid-repository.json new file mode 100644 index 0000000..8457345 --- /dev/null +++ b/contracts/repository/v1/fixtures/valid-repository.json @@ -0,0 +1 @@ +{"compression":"none","created_at":"2026-07-27T00:00:00Z","digest_algorithm":"sha256","encryption":{"key_id":null,"mode":"none"},"format_version":1,"repository_id":"0198c57f-0000-7000-8000-000000000001"} diff --git a/contracts/repository/v1/manifest.schema.json b/contracts/repository/v1/manifest.schema.json new file mode 100644 index 0000000..79ede3a --- /dev/null +++ b/contracts/repository/v1/manifest.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://backup-tool.invalid/contracts/repository/v1/manifest.schema.json", + "title": "Backup Tool Manifest v1", + "type": "object", + "additionalProperties": false, + "required": [ + "format_version", "backup_id", "repository_id", "source_id", "job_id", "execution_id", + "requested_mode", "effective_mode", "created_at", "source_consistency", "exclusion_policy", + "entries", "aggregates", "encryption_key_id", "manifest_digest" + ], + "properties": { + "format_version": {"const": 1}, + "backup_id": {"type": "string", "format": "uuid"}, + "repository_id": {"type": "string", "format": "uuid"}, + "source_id": {"type": "string", "format": "uuid"}, + "job_id": {"type": "string", "format": "uuid"}, + "execution_id": {"type": "string", "format": "uuid"}, + "requested_mode": {"enum": ["full", "incremental"]}, + "effective_mode": {"enum": ["full", "incremental"]}, + "created_at": {"type": "string", "format": "date-time"}, + "source_consistency": {"type": "object"}, + "exclusion_policy": { + "type": "object", + "additionalProperties": false, + "required": ["matcher", "version", "patterns"], + "properties": { + "matcher": {"const": "gitignore"}, + "version": {"type": "integer", "minimum": 1}, + "patterns": {"type": "array", "items": {"type": "string"}} + } + }, + "entries": { + "type": "array", + "items": {"$ref": "#/$defs/entry"} + }, + "aggregates": { + "type": "object", + "additionalProperties": false, + "required": ["entry_count", "logical_bytes", "stored_bytes"], + "properties": { + "entry_count": {"type": "integer", "minimum": 0}, + "logical_bytes": {"type": "integer", "minimum": 0}, + "stored_bytes": {"type": "integer", "minimum": 0} + } + }, + "encryption_key_id": {"type": ["string", "null"]}, + "manifest_digest": {"type": "string", "pattern": "^[0-9a-f]{64}$"} + }, + "$defs": { + "entry": { + "type": "object", + "additionalProperties": false, + "required": ["path", "type", "size", "blob_digest", "mode", "mtime_ns", "link_target", "metadata_support"], + "properties": { + "path": {"type": "string", "minLength": 1, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$"}, + "type": {"enum": ["file", "directory", "symlink"]}, + "size": {"type": "integer", "minimum": 0}, + "blob_digest": {"type": ["string", "null"], "pattern": "^[0-9a-f]{64}$"}, + "mode": {"type": ["integer", "null"], "minimum": 0}, + "mtime_ns": {"type": ["integer", "null"], "minimum": 0}, + "link_target": {"type": ["string", "null"]}, + "metadata_support": {"type": "array", "items": {"type": "string"}, "uniqueItems": true} + } + } + } +} diff --git a/contracts/repository/v1/normalized-paths.json b/contracts/repository/v1/normalized-paths.json new file mode 100644 index 0000000..0f202c2 --- /dev/null +++ b/contracts/repository/v1/normalized-paths.json @@ -0,0 +1,9 @@ +[ + {"raw": "file.txt", "normalized": "file.txt", "valid": true}, + {"raw": "dir/./file.txt", "normalized": "dir/file.txt", "valid": true}, + {"raw": "unicodé/文件.txt", "normalized": "unicodé/文件.txt", "valid": true}, + {"raw": "/absolute", "normalized": null, "valid": false}, + {"raw": "../escape", "normalized": null, "valid": false}, + {"raw": "a\\b", "normalized": null, "valid": false}, + {"raw": "", "normalized": null, "valid": false} +] diff --git a/contracts/repository/v1/repository.schema.json b/contracts/repository/v1/repository.schema.json new file mode 100644 index 0000000..8093ea7 --- /dev/null +++ b/contracts/repository/v1/repository.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://backup-tool.invalid/contracts/repository/v1/repository.schema.json", + "title": "Backup Tool Repository v1", + "type": "object", + "additionalProperties": false, + "required": ["repository_id", "format_version", "digest_algorithm", "compression", "encryption", "created_at"], + "properties": { + "repository_id": {"type": "string", "format": "uuid"}, + "format_version": {"const": 1}, + "digest_algorithm": {"const": "sha256"}, + "compression": {"enum": ["none", "zstd"]}, + "encryption": { + "type": "object", + "additionalProperties": false, + "required": ["mode", "key_id"], + "properties": { + "mode": {"enum": ["none", "aes-256-gcm"]}, + "key_id": {"type": ["string", "null"], "minLength": 1} + } + }, + "created_at": {"type": "string", "format": "date-time"} + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..cc96aa3 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3670 @@ +{ + "name": "backup-tool-frontend", + "version": "2.0.0-dev.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "backup-tool-frontend", + "version": "2.0.0-dev.0", + "dependencies": { + "react": "19.2.8", + "react-dom": "19.2.8" + }, + "devDependencies": { + "@testing-library/jest-dom": "7.0.0", + "@testing-library/react": "16.3.2", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-react": "6.0.4", + "autoprefixer": "10.5.4", + "jsdom": "30.0.0", + "postcss": "8.5.23", + "tailwindcss": "3.4.19", + "typescript": "7.0.2", + "vite": "8.1.5", + "vitest": "4.1.10" + }, + "engines": { + "node": "22.17.1" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.5.tgz", + "integrity": "sha512-mbhpPMmnw/kwW19aRNmSUl1QzLbdGo1SCuE49BT98MNwqF6zaHb3o2owssFc/PEO/4t2UjqtCNwocuDtJornzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.2.1", + "@csstools/css-color-parser": "^4.1.9", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.0.tgz", + "integrity": "sha512-UJLfKXBhrc8i1vH2eJXuYQMwlsLKWFw3O+CPqXSuVEiikeAim3UgrfWX0k4tA/X8cRFM8iZ7OaqBokFGbYusdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.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", + "peer": true, + "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/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", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", + "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", + "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "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/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/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "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/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@testing-library/jest-dom": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.0.tgz", + "integrity": "sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz", + "integrity": "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "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/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz", + "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==", + "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.28.6", + "caniuse-lite": "^1.0.30001806", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.5.tgz", + "integrity": "sha512-xJo6a6YZnwZfnyGmQKWMbVOcii7XRibjOskRh+WJ9UHQoX16xrQrcIgAMQOzfvs8XiLMx6ih/fsLPF73iY2D1A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "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/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.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "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.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "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/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/chokidar/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/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "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/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.396", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz", + "integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "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/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "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.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/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/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/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/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "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/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/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "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/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.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-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-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-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "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", + "peer": true + }, + "node_modules/jsdom": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.0.tgz", + "integrity": "sha512-JQHfRGmmKmaZoUAvIgff5jjG/0SzTQlGz8c7t72KzBzo8ZULEjAjnYE0sNwBOUA4QtWwYE2xoYitg8NFsmiYxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^6.0.5", + "@asamuzakjp/dom-selector": "^8.2.5", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.6", + "@exodus/bytes": "^1.15.1", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.2", + "undici": "^8.7.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "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/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "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/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/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "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/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/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/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "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.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "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/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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/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/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "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/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/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/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/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "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/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "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/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "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-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.9.tgz", + "integrity": "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.9" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.9.tgz", + "integrity": "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==", + "dev": true, + "license": "MIT" + }, + "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/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "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==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz", + "integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "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/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/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz", + "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/frontend/package.json b/frontend/package.json index 3518745..e455079 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,36 +1,35 @@ { "name": "backup-tool-frontend", "private": true, - "version": "0.0.1", + "version": "2.0.0-dev.0", "type": "module", + "packageManager": "npm@10.9.2", + "engines": { + "node": "22.17.1" + }, "scripts": { "dev": "vite", + "typecheck": "tsc --noEmit", + "test": "vitest run --passWithNoTests", "build": "tsc && vite build", "preview": "vite preview" }, "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.21.0", - "@tanstack/react-query": "^5.17.0", - "axios": "^1.6.5", - "react-hook-form": "^7.49.0", - "recharts": "^2.10.0", - "lucide-react": "^0.303.0", - "clsx": "^2.1.0" + "react": "19.2.8", + "react-dom": "19.2.8" }, "devDependencies": { - "@types/react": "^18.2.43", - "@types/react-dom": "^18.2.17", - "@vitejs/plugin-react": "^4.2.1", - "autoprefixer": "^10.4.16", - "postcss": "^8.4.32", - "tailwindcss": "^3.4.0", - "typescript": "^5.2.2", - "vite": "^5.0.8", - "vitest": "^1.1.0", - "@testing-library/react": "^14.1.0", - "@testing-library/jest-dom": "^6.2.0", - "jsdom": "^23.0.0" + "@testing-library/jest-dom": "7.0.0", + "@testing-library/react": "16.3.2", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-react": "6.0.4", + "autoprefixer": "10.5.4", + "jsdom": "30.0.0", + "postcss": "8.5.23", + "tailwindcss": "3.4.19", + "typescript": "7.0.2", + "vite": "8.1.5", + "vitest": "4.1.10" } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index 02ff4f4..0000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { BrowserRouter, Routes, Route } from 'react-router-dom'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { Layout } from './components/Layout'; -import { Dashboard } from './pages/Dashboard'; -import { Backups } from './pages/Backups'; -import { Settings } from './pages/Settings'; - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 5 * 60 * 1000, - retry: 1, - }, - }, -}); - -function App() { - return ( - - - - - } /> - } /> - } /> - - - - - ); -} - -export default App; diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts deleted file mode 100644 index 3038927..0000000 --- a/frontend/src/api/client.ts +++ /dev/null @@ -1,62 +0,0 @@ -import axios from 'axios'; - -const API_BASE_URL = '/api'; - -export const apiClient = axios.create({ - baseURL: API_BASE_URL, - headers: { - 'Content-Type': 'application/json', - }, -}); - -export interface BackupSource { - id: string; - name: string; - type: string; - config: Record; - created_at: string; - updated_at: string; -} - -export interface BackupJob { - id: string; - source_id: string; - status: 'pending' | 'running' | 'completed' | 'failed'; - started_at: string | null; - completed_at: string | null; - error_message: string | null; - created_at: string; -} - -export interface DashboardStats { - total_sources: number; - total_jobs: number; - completed_jobs: number; - failed_jobs: number; - pending_jobs: number; -} - -export const sourcesApi = { - getAll: () => apiClient.get('/sources'), - getById: (id: string) => apiClient.get(`/sources/${id}`), - create: (data: Omit) => - apiClient.post('/sources', data), - update: (id: string, data: Partial) => - apiClient.put(`/sources/${id}`, data), - delete: (id: string) => apiClient.delete(`/sources/${id}`), -}; - -export const jobsApi = { - getAll: () => apiClient.get('/jobs'), - getById: (id: string) => apiClient.get(`/jobs/${id}`), - create: (sourceId: string) => - apiClient.post('/jobs', { source_id: sourceId }), - delete: (id: string) => apiClient.delete(`/jobs/${id}`), - getLogs: (id: string) => apiClient.get(`/jobs/${id}/logs`), -}; - -export const dashboardApi = { - getStats: () => apiClient.get('/dashboard/stats'), - getRecentJobs: (limit: number = 10) => - apiClient.get(`/dashboard/recent-jobs?limit=${limit}`), -}; diff --git a/frontend/src/api/generated/.gitkeep b/frontend/src/api/generated/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx new file mode 100644 index 0000000..a1f9b2f --- /dev/null +++ b/frontend/src/app/App.tsx @@ -0,0 +1,16 @@ +export function App() { + return ( +
+
+

+ Backup Tool v2 +

+

Protocol foundation ready

+

+ Operator workflows are added as their versioned API contracts become + executable. +

+
+
+ ); +} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx deleted file mode 100644 index 76c714e..0000000 --- a/frontend/src/components/Layout.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Link, useLocation } from 'react-router-dom'; -import { LayoutDashboard, Archive, Settings, Menu } from 'lucide-react'; -import { useState } from 'react'; - -interface LayoutProps { - children: React.ReactNode; -} - -const navItems = [ - { path: '/', label: 'Dashboard', icon: LayoutDashboard }, - { path: '/backups', label: 'Backups', icon: Archive }, - { path: '/settings', label: 'Settings', icon: Settings }, -]; - -export function Layout({ children }: LayoutProps) { - const location = useLocation(); - const [sidebarOpen, setSidebarOpen] = useState(false); - - return ( -
- {/* Mobile sidebar overlay */} - {sidebarOpen && ( -
setSidebarOpen(false)} - /> - )} - - {/* Sidebar */} - - - {/* Main content */} -
- {/* Mobile header */} -
- - - Backup Tool - -
- -
{children}
-
-
- ); -} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 964aeb4..e3e15cc 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,10 +1,17 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App' -import './index.css' +import React from "react"; +import ReactDOM from "react-dom/client"; -ReactDOM.createRoot(document.getElementById('root')!).render( - - - , -) +import { App } from "./app/App"; +import "./index.css"; + +const root = document.getElementById("root"); + +if (!root) { + throw new Error("Missing #root element"); +} + +ReactDOM.createRoot(root).render( + + + , +); diff --git a/frontend/src/pages/Backups.tsx b/frontend/src/pages/Backups.tsx deleted file mode 100644 index f19a298..0000000 --- a/frontend/src/pages/Backups.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import { useState } from 'react'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { Play, Trash2, Plus } from 'lucide-react'; -import { jobsApi, sourcesApi } from '../api/client'; -import type { BackupJob, BackupSource } from '../api/client'; - -function StatusBadge({ status }: { status: string }) { - const styles = { - pending: 'bg-yellow-100 text-yellow-800', - running: 'bg-blue-100 text-blue-800', - completed: 'bg-green-100 text-green-800', - failed: 'bg-red-100 text-red-800', - }; - return ( - - {status} - - ); -} - -function CreateJobModal({ - onClose, -}: { - onClose: () => void; -}) { - const queryClient = useQueryClient(); - const [sourceId, setSourceId] = useState(''); - - const { data: sources } = useQuery({ - queryKey: ['sources'], - queryFn: () => sourcesApi.getAll().then((res) => res.data), - }); - - const createMutation = useMutation({ - mutationFn: (sid: string) => jobsApi.create(sid), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['jobs'] }); - onClose(); - }, - }); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (sourceId) { - createMutation.mutate(sourceId); - } - }; - - return ( -
-
-
-

- Create Backup Job -

-
-
-
- - -
-
- - -
-
-
-
- ); -} - -export function Backups() { - const [showModal, setShowModal] = useState(false); - const queryClient = useQueryClient(); - - const { data: jobs, isLoading } = useQuery({ - queryKey: ['jobs'], - queryFn: () => jobsApi.getAll().then((res) => res.data), - }); - - const { data: sources } = useQuery({ - queryKey: ['sources'], - queryFn: () => sourcesApi.getAll().then((res) => res.data), - }); - - const deleteMutation = useMutation({ - mutationFn: (id: string) => jobsApi.delete(id), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['jobs'] }); - }, - }); - - const runMutation = useMutation({ - mutationFn: (id: string) => jobsApi.create(id), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['jobs'] }); - }, - }); - - const getSourceName = (sourceId: string) => { - const source = sources?.find((s: BackupSource) => s.id === sourceId); - return source?.name || sourceId.slice(0, 8); - }; - - return ( -
-
-

Backups

- -
- -
- - - - - - - - - - - - {isLoading && ( - - - - )} - {jobs?.length === 0 && !isLoading && ( - - - - )} - {jobs?.map((job: BackupJob) => ( - - - - - - - - ))} - -
- Job ID - - Source - - Status - - Started - - Actions -
- Loading... -
- No backup jobs yet. Create one to get started. -
- {job.id.slice(0, 8)} - - {getSourceName(job.source_id)} - - - - {job.started_at - ? new Date(job.started_at).toLocaleString() - : 'Not started'} - -
- - -
-
-
- - {showModal && setShowModal(false)} />} -
- ); -} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx deleted file mode 100644 index ffcd5de..0000000 --- a/frontend/src/pages/Dashboard.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { Activity, Archive, AlertTriangle, CheckCircle } from 'lucide-react'; -import { dashboardApi } from '../api/client'; - -function StatCard({ - title, - value, - icon: Icon, - color, -}: { - title: string; - value: number; - icon: React.ElementType; - color: string; -}) { - return ( -
-
-
-

{title}

-

{value}

-
-
- -
-
-
- ); -} - -function StatusBadge({ status }: { status: string }) { - const styles = { - pending: 'bg-yellow-100 text-yellow-800', - running: 'bg-blue-100 text-blue-800', - completed: 'bg-green-100 text-green-800', - failed: 'bg-red-100 text-red-800', - }; - return ( - - {status} - - ); -} - -export function Dashboard() { - const { data: stats } = useQuery({ - queryKey: ['dashboard-stats'], - queryFn: () => dashboardApi.getStats().then((res) => res.data), - }); - - const { data: recentJobs } = useQuery({ - queryKey: ['recent-jobs'], - queryFn: () => dashboardApi.getRecentJobs(5).then((res) => res.data), - }); - - const activeJobs = stats?.pending_jobs || 0; - const recentFailures = stats?.failed_jobs || 0; - - return ( -
-

Dashboard

- -
- - - - -
- -
-
-

- Recent Activity -

-
-
- {recentJobs?.length === 0 && ( -
- No recent activity -
- )} - {recentJobs?.map((job) => ( -
-
-
-
-

- Job {job.id.slice(0, 8)} -

-

- {job.created_at - ? new Date(job.created_at).toLocaleString() - : 'Unknown'} -

-
-
- -
- ))} -
-
-
- ); -} diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx deleted file mode 100644 index 5076a7f..0000000 --- a/frontend/src/pages/Settings.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import { useState } from 'react'; - -const tabs = [ - { id: 'general', label: 'General' }, - { id: 'notifications', label: 'Notifications' }, - { id: 'security', label: 'Security' }, - { id: 'logs', label: 'Logs' }, -]; - -function GeneralSettings() { - return ( -
-
- - -
-
- - -
-
- - -
-
- ); -} - -function NotificationSettings() { - return ( -
-
- - -
-
- - -
-
- - -
-
- ); -} - -function SecuritySettings() { - return ( -
-
- - -
-
- - -
-
- - -
-
- ); -} - -function LogSettings() { - return ( -
-
- - -
-
- - -
-
- - -
-
- ); -} - -export function Settings() { - const [activeTab, setActiveTab] = useState('general'); - - const tabContent = { - general: , - notifications: , - security: , - logs: , - }; - - return ( -
-

Settings

- -
-
- -
-
{tabContent[activeTab as keyof typeof tabContent]}
-
-
- ); -} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tests/contract/test_no_v1.py b/tests/contract/test_no_v1.py index 7691090..e907778 100644 --- a/tests/contract/test_no_v1.py +++ b/tests/contract/test_no_v1.py @@ -1,9 +1,8 @@ from __future__ import annotations -from pathlib import Path import subprocess import sys - +from pathlib import Path ROOT = Path(__file__).parents[2] diff --git a/tests/contract/test_repository_format.py b/tests/contract/test_repository_format.py index 1fddcff..6aed68d 100644 --- a/tests/contract/test_repository_format.py +++ b/tests/contract/test_repository_format.py @@ -7,7 +7,6 @@ from typing import Any import jsonschema import pytest - ROOT = Path(__file__).parents[2] CONTRACT = ROOT / "contracts" / "repository" / "v1" FIXTURES = CONTRACT / "fixtures" diff --git a/tests/e2e/.gitkeep b/tests/e2e/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/fault/.gitkeep b/tests/fault/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/.gitkeep b/tests/fixtures/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/.gitkeep b/tests/integration/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/performance/.gitkeep b/tests/performance/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/security/.gitkeep b/tests/security/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/.gitkeep b/tests/unit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tools/forbidden_v1_scan.py b/tools/forbidden_v1_scan.py new file mode 100644 index 0000000..66cd07d --- /dev/null +++ b/tools/forbidden_v1_scan.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Reject executable v1 compatibility paths and symbols from the v2 tree.""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +LEGACY_PATHS = ( + Path("backend/app"), + Path("backend/backup"), + Path("backend/tests"), + Path("frontend/src/api/client.ts"), +) +SCAN_ROOTS = (Path("backend/src"), Path("frontend/src"), Path("openapi")) +TEXT_SUFFIXES = {".json", ".py", ".ts", ".tsx", ".yaml", ".yml"} +FORBIDDEN = ( + re.compile(r"/api/v1(?:/|\b)"), + re.compile(r"\blegacy_(?:reader|importer?|converter?)\b", re.IGNORECASE), + re.compile(r"\btimestamp_directory\b", re.IGNORECASE), + re.compile(r"\b(?:app\.main|backup\.engine)\b"), +) + + +def scan(root: Path) -> list[str]: + findings: list[str] = [] + for relative in LEGACY_PATHS: + if (root / relative).exists(): + findings.append(f"legacy path exists: {relative}") + + for relative_root in SCAN_ROOTS: + scan_root = root / relative_root + if not scan_root.exists(): + continue + for path in sorted(scan_root.rglob("*")): + if not path.is_file() or path.suffix not in TEXT_SUFFIXES: + continue + try: + text = path.read_text(encoding="utf-8") + except (OSError, UnicodeError) as error: + findings.append(f"cannot read {path.relative_to(root)}: {error}") + continue + for pattern in FORBIDDEN: + if pattern.search(text): + findings.append( + f"forbidden symbol {pattern.pattern!r}: {path.relative_to(root)}" + ) + return findings + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("root", nargs="?", default=".", type=Path) + args = parser.parse_args() + root = args.root.resolve() + findings = scan(root) + if findings: + print("v1 compatibility scan failed:", file=sys.stderr) + for finding in findings: + print(f"- {finding}", file=sys.stderr) + return 1 + print("v1 compatibility scan: OK") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())