From 6ddf55fe82bb25f2809b9bdc1602b8a317573cc9 Mon Sep 17 00:00:00 2001 From: KazzKondo Date: Fri, 10 Jul 2026 13:52:43 +0900 Subject: [PATCH 1/2] Implement MVP: backend, auth, AI sidebar, and test fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the full implementation across parts 2–10 of the plan: - FastAPI backend with SQLite persistence, board CRUD, and AI board-action endpoint - Docker and startup/stop scripts for local development - Next.js static export wired to the backend, with AuthGate and AI assistant sidebar - Backend and frontend tests covering persistence, auth, AI connectivity, and board actions - Fixed AI board action tests to patch OPENROUTER_API_KEY via os.environ before mocking httpx - Added CLAUDE.md with commands, architecture overview, and milestone status Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 124 +++ Dockerfile | 17 + backend/AGENTS.md | 25 +- backend/__init__.py | 0 backend/database.py | 237 ++++++ backend/main.py | 211 +++++ backend/pyproject.toml | 9 + backend/requirements-dev.txt | 2 + backend/requirements.txt | 3 + backend/tests/test_ai_board_actions.py | 62 ++ backend/tests/test_ai_connectivity.py | 34 + backend/tests/test_frontend_serving.py | 14 + backend/tests/test_persistence.py | 43 + docker-compose.yml | 9 + docs/PLAN.md | 272 ++++++- docs/database-approach.md | 32 + docs/database-schema.json | 54 ++ frontend/AGENTS.md | 43 + frontend/next.config.ts | 3 +- frontend/package-lock.json | 739 ++++++++++-------- frontend/package.json | 1 + frontend/src/app/page.tsx | 4 +- .../src/components/AIAssistantSidebar.tsx | 131 ++++ frontend/src/components/AuthGate.test.tsx | 42 + frontend/src/components/AuthGate.tsx | 115 +++ frontend/src/components/KanbanBoard.test.tsx | 128 ++- frontend/src/components/KanbanBoard.tsx | 144 +++- scripts/AGENTS.md | 19 +- scripts/start.ps1 | 32 + scripts/start.sh | 22 + scripts/stop.ps1 | 6 + scripts/stop.sh | 3 + 32 files changed, 2197 insertions(+), 383 deletions(-) create mode 100644 CLAUDE.md create mode 100644 Dockerfile create mode 100644 backend/__init__.py create mode 100644 backend/database.py create mode 100644 backend/main.py create mode 100644 backend/pyproject.toml create mode 100644 backend/requirements-dev.txt create mode 100644 backend/requirements.txt create mode 100644 backend/tests/test_ai_board_actions.py create mode 100644 backend/tests/test_ai_connectivity.py create mode 100644 backend/tests/test_frontend_serving.py create mode 100644 backend/tests/test_persistence.py create mode 100644 docker-compose.yml create mode 100644 docs/database-approach.md create mode 100644 docs/database-schema.json create mode 100644 frontend/AGENTS.md create mode 100644 frontend/src/components/AIAssistantSidebar.tsx create mode 100644 frontend/src/components/AuthGate.test.tsx create mode 100644 frontend/src/components/AuthGate.tsx create mode 100644 scripts/start.ps1 create mode 100644 scripts/start.sh create mode 100644 scripts/stop.ps1 create mode 100644 scripts/stop.sh diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..136e60a6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,124 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project overview + +A Kanban-based project management MVP with an AI assistant sidebar. The stack is: + +- **Frontend**: Next.js 16 (static export via `output: "export"`) with React 19, dnd-kit for drag-and-drop, Tailwind CSS +- **Backend**: FastAPI + Uvicorn, Python 3.11+, SQLite persistence via `pm.sqlite` at the repo root +- **AI**: OpenRouter (configured via `OPENROUTER_API_KEY` and `OPENROUTER_MODEL` env vars) + +The backend serves the pre-built Next.js static export from `frontend/out/` at `/`, and all API routes are under `/api/`. + +## Development commands + +### Start the app (Windows) + +```powershell +.\scripts\start.ps1 +``` + +This installs Python deps with `uv`, builds the Next.js frontend (`npm run build`), then starts Uvicorn at `http://127.0.0.1:8000`. + +### Start the app (macOS/Linux) + +```bash +./scripts/start.sh +``` + +### Docker + +```bash +docker compose up --build +``` + +### Frontend development only + +```bash +cd frontend +npm install +npm run dev # dev server at http://localhost:3000 +npm run build # static export to frontend/out/ +``` + +### Backend development only + +```powershell +# From repo root, with .venv active: +$env:PYTHONPATH = "." +.\.venv\Scripts\python.exe -m uvicorn backend.main:app --host 127.0.0.1 --port 8000 --reload +``` + +## Testing + +### Frontend unit tests (Vitest) + +```bash +cd frontend +npx vitest run # run all +npx vitest run --reporter=verbose +npx vitest run src/components/KanbanBoard.test.tsx # single file +``` + +### Frontend E2E tests (Playwright) + +```bash +cd frontend +npx playwright test +``` + +### Backend tests (pytest) + +```bash +# From repo root, with .venv active and PYTHONPATH=. +$env:PYTHONPATH = "." +.\.venv\Scripts\python.exe -m pytest backend/tests/ +.\.venv\Scripts\python.exe -m pytest backend/tests/test_persistence.py # single file +``` + +Dev dependencies for the backend are in `backend/requirements-dev.txt` (`pytest`, `httpx`). + +## Architecture + +### Authentication + +`AuthGate` (`frontend/src/components/AuthGate.tsx`) wraps the entire app. It shows a login form before rendering `KanbanBoard`. The dummy credentials are `user` / `password`. On login the username is passed down to `KanbanBoard` and used in all `/api/board/{username}` calls. + +### Board data flow + +1. `KanbanBoard` loads board state from `GET /api/board/{username}` on mount. +2. All mutations (drag-and-drop, card create/rename/delete) call `PUT /api/board/{username}` with the full updated board payload. +3. The AI sidebar (`AIAssistantSidebar`) posts user messages to `POST /api/ai/board-action`. When the AI response includes a `board_update`, the backend applies it and returns the new board state, which the frontend uses to refresh the UI. + +### Board payload shape + +```json +{ + "columns": [{ "id": "col-1", "title": "Backlog", "cardIds": ["card-1"] }], + "cards": { "card-1": { "id": "card-1", "title": "...", "details": "..." } } +} +``` + +### Database + +`backend/database.py` manages a single SQLite file (`pm.sqlite`) at the repo root. `ensure_seed_data()` is called on every read/write — it bootstraps the schema and seeds the demo user (`user`) plus a default board on first run. `replace_board_state` does a delete-and-reinsert of all columns and cards for a given user's board. + +### Backend API routes + +| Method | Path | Purpose | +|--------|------|---------| +| GET | `/api/health` | Health check | +| GET | `/api/board/{username}` | Fetch board state | +| PUT | `/api/board/{username}` | Replace full board state | +| GET | `/api/ai/connectivity` | Test OpenRouter connection | +| POST | `/api/ai/board-action` | AI chat + optional board update | + +### Frontend shared logic + +`frontend/src/lib/kanban.ts` holds `BoardData` types, `initialData` (fallback when API is unavailable), `moveCard` helper, and `createId`. + +## Implementation milestones (from `docs/PLAN.md`) + +Parts 1–5 (planning, scaffolding, frontend integration, auth, DB schema) are complete. Parts 6–10 (backend persistence, full integration, AI connectivity, structured AI actions, AI sidebar) are in progress or planned — check `docs/PLAN.md` for the current checklist state. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..020000c8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +RUN pip install --no-cache-dir uv + +WORKDIR /app + +COPY backend/requirements.txt /app/backend/requirements.txt +RUN uv pip install --system -r /app/backend/requirements.txt + +COPY . /app + +EXPOSE 8000 + +CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/AGENTS.md b/backend/AGENTS.md index 6d2147f0..f0f52d6b 100644 --- a/backend/AGENTS.md +++ b/backend/AGENTS.md @@ -1 +1,24 @@ -This file should be updated with a description of the Backend \ No newline at end of file +# Backend agent guide + +## Purpose + +This directory contains the FastAPI backend for the project management MVP. The initial scaffold provides a minimal app entry point and a small set of example routes that can be expanded into the full persistence and AI workflow. + +## Current structure + +- main.py: FastAPI application entry point with health and demo endpoints. +- requirements.txt: Python dependencies for the backend. +- pyproject.toml: Optional project metadata for the backend. + +## Current implementation notes + +- The backend is intentionally minimal at this stage. +- The app exposes a health endpoint at /api/health and a demo route at /api/demo. +- The expected runtime target is a local containerized deployment, but the app can also run directly with Uvicorn. + +## Working conventions + +- Keep the backend simple and aligned with the MVP scope. +- Prefer small, testable endpoints over broad abstractions. +- Add tests when new routes or persistence logic are introduced. +- Follow the repository guidance to investigate root causes before changing behavior. diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/database.py b/backend/database.py new file mode 100644 index 00000000..bc92758d --- /dev/null +++ b/backend/database.py @@ -0,0 +1,237 @@ +import sqlite3 +from pathlib import Path +from typing import Any + +DB_PATH = Path(__file__).resolve().parent.parent / "pm.sqlite" + + +def get_connection() -> sqlite3.Connection: + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA foreign_keys = ON") + return conn + + +def init_db() -> None: + conn = get_connection() + try: + conn.executescript( + """ + DROP TABLE IF EXISTS cards; + DROP TABLE IF EXISTS columns; + DROP TABLE IF EXISTS boards; + DROP TABLE IF EXISTS users; + + CREATE TABLE users ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + created_at TEXT NOT NULL + ); + + CREATE TABLE boards ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL UNIQUE REFERENCES users(id), + title TEXT NOT NULL DEFAULT 'Project Board', + created_at TEXT NOT NULL + ); + + CREATE TABLE columns ( + id TEXT PRIMARY KEY, + board_id TEXT NOT NULL REFERENCES boards(id) ON DELETE CASCADE, + position INTEGER NOT NULL, + title TEXT NOT NULL + ); + + CREATE TABLE cards ( + id TEXT PRIMARY KEY, + board_id TEXT NOT NULL REFERENCES boards(id) ON DELETE CASCADE, + column_id TEXT NOT NULL REFERENCES columns(id) ON DELETE CASCADE, + position INTEGER NOT NULL, + title TEXT NOT NULL, + details TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL + ); + """ + ) + conn.commit() + finally: + conn.close() + + +def ensure_seed_data() -> None: + conn = get_connection() + try: + existing_tables = conn.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name IN ('users', 'boards', 'columns', 'cards')" + ).fetchall() + if len(existing_tables) != 4: + conn.close() + init_db() + return + + user_row = conn.execute( + "SELECT id FROM users WHERE username = ?", + ("user",), + ).fetchone() + if user_row is not None: + return + + user_id = "user-1" + conn.execute( + "INSERT INTO users (id, username, password_hash, created_at) VALUES (?, ?, ?, datetime('now'))", + (user_id, "user", "demo-password"), + ) + board_id = "board-1" + conn.execute( + "INSERT INTO boards (id, user_id, title, created_at) VALUES (?, ?, ?, datetime('now'))", + (board_id, user_id, "Project Board"), + ) + columns = [ + ("col-1", "Backlog", 0), + ("col-2", "Discovery", 1), + ("col-3", "In Progress", 2), + ("col-4", "Review", 3), + ("col-5", "Done", 4), + ] + for column_id, title, position in columns: + conn.execute( + "INSERT INTO columns (id, board_id, position, title) VALUES (?, ?, ?, ?)", + (column_id, board_id, position, title), + ) + column_ids = [column_id for column_id, _, _ in columns] + cards = [ + ("card-1", column_ids[0], 0, "Align roadmap themes", "Draft quarterly themes with impact statements and metrics."), + ("card-2", column_ids[0], 1, "Gather customer signals", "Review support tags, sales notes, and churn feedback."), + ("card-3", column_ids[1], 0, "Prototype analytics view", "Sketch initial dashboard layout and key drill-downs."), + ("card-4", column_ids[2], 0, "Refine status language", "Standardize column labels and tone across the board."), + ("card-5", column_ids[2], 1, "Design card layout", "Add hierarchy and spacing for scanning dense lists."), + ("card-6", column_ids[3], 0, "QA micro-interactions", "Verify hover, focus, and loading states."), + ("card-7", column_ids[4], 0, "Ship marketing page", "Final copy approved and asset pack delivered."), + ("card-8", column_ids[4], 1, "Close onboarding sprint", "Document release notes and share internally."), + ] + for card_id, column_id, position, title, details in cards: + conn.execute( + "INSERT INTO cards (id, board_id, column_id, position, title, details, created_at) VALUES (?, ?, ?, ?, ?, ?, datetime('now'))", + (card_id, board_id, column_id, position, title, details), + ) + conn.commit() + finally: + conn.close() + + +def get_board_state(username: str) -> dict[str, Any]: + ensure_seed_data() + conn = get_connection() + try: + user_row = conn.execute( + "SELECT id FROM users WHERE username = ?", + (username,), + ).fetchone() + if user_row is None: + raise ValueError("User not found") + + board_row = conn.execute( + "SELECT id, title FROM boards WHERE user_id = ?", + (user_row["id"],), + ).fetchone() + if board_row is None: + raise ValueError("Board not found") + + columns = conn.execute( + "SELECT id, title FROM columns WHERE board_id = ? ORDER BY position ASC", + (board_row["id"],), + ).fetchall() + cards = conn.execute( + "SELECT id, column_id, title, details FROM cards WHERE board_id = ? ORDER BY position ASC", + (board_row["id"],), + ).fetchall() + + card_map: dict[str, dict[str, Any]] = {} + for card in cards: + card_map[str(card["id"])] = { + "id": str(card["id"]), + "title": card["title"], + "details": card["details"], + } + + column_list: list[dict[str, Any]] = [] + for column in columns: + column_card_ids = [ + str(card["id"]) + for card in cards + if card["column_id"] == column["id"] + ] + column_list.append( + { + "id": str(column["id"]), + "title": column["title"], + "cardIds": column_card_ids, + } + ) + + return {"columns": column_list, "cards": card_map} + finally: + conn.close() + + +def replace_board_state(username: str, board_data: dict[str, Any] | Any) -> dict[str, Any]: + ensure_seed_data() + conn = get_connection() + try: + user_row = conn.execute( + "SELECT id FROM users WHERE username = ?", + (username,), + ).fetchone() + if user_row is None: + raise ValueError("User not found") + + board_row = conn.execute( + "SELECT id FROM boards WHERE user_id = ?", + (user_row["id"],), + ).fetchone() + if board_row is None: + raise ValueError("Board not found") + + board_id = board_row["id"] + conn.execute("DELETE FROM cards WHERE board_id = ?", (board_id,)) + conn.execute("DELETE FROM columns WHERE board_id = ?", (board_id,)) + conn.commit() + + if hasattr(board_data, "model_dump"): + board_data = board_data.model_dump() + elif not isinstance(board_data, dict): + board_data = dict(board_data) + + columns = board_data.get("columns", []) + cards = board_data.get("cards", {}) + + for position, column in enumerate(columns): + column_dict = column if isinstance(column, dict) else {} + column_id = str(column_dict.get("id") or f"col-{position + 1}") + conn.execute( + "INSERT INTO columns (id, board_id, position, title) VALUES (?, ?, ?, ?)", + (column_id, board_id, position, column_dict.get("title", "Untitled")), + ) + card_ids = column_dict.get("cardIds", []) + for card_position, card_id in enumerate(card_ids): + card = cards.get(card_id) + if card is None: + continue + card_dict = card if isinstance(card, dict) else {} + conn.execute( + "INSERT INTO cards (id, board_id, column_id, position, title, details, created_at) VALUES (?, ?, ?, ?, ?, ?, datetime('now'))", + ( + str(card_id), + board_id, + column_id, + card_position, + card_dict.get("title", "Untitled"), + card_dict.get("details", ""), + ), + ) + + conn.commit() + return get_board_state(username) + finally: + conn.close() diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 00000000..34b9c8d4 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,211 @@ +import json +import os +from pathlib import Path +from typing import Any + +import httpx +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from starlette.responses import FileResponse + +from backend.database import get_board_state, replace_board_state + +app = FastAPI(title="Project Management MVP Backend", version="0.1.0") + +APP_ROOT = Path(__file__).resolve().parent.parent +FRONTEND_BUILD_DIR = APP_ROOT / "frontend" / "out" + + +class BoardPayload(BaseModel): + columns: list[dict[str, object]] + cards: dict[str, dict[str, str]] + + +class AIConnectivityResponse(BaseModel): + ok: bool + message: str + model: str | None = None + provider: str | None = None + + +class AIBoardActionRequest(BaseModel): + username: str + question: str + history: list[dict[str, str]] | None = None + + +class AIBoardActionResponse(BaseModel): + reply: str + applied: bool + board: dict[str, Any] | None = None + + +class AIBoardUpdatePayload(BaseModel): + reply: str + board_update: BoardPayload | None = None + + +@app.get("/", include_in_schema=False) +async def read_root() -> FileResponse: + return FileResponse(FRONTEND_BUILD_DIR / "index.html") + + +@app.get("/api/health") +async def health_check() -> dict[str, str]: + return {"status": "ok", "service": "pm-backend"} + + +@app.get("/api/demo") +async def demo_route() -> dict[str, str]: + return {"message": "Hello from the API", "app": "Project Management MVP"} + + +@app.get("/api/board/{username}") +async def get_board(username: str) -> dict[str, object]: + try: + return get_board_state(username) + except ValueError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + +@app.put("/api/board/{username}") +async def update_board(username: str, payload: BoardPayload) -> dict[str, object]: + try: + return replace_board_state(username, payload.model_dump()) + except ValueError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + +@app.get("/api/ai/connectivity") +async def ai_connectivity() -> AIConnectivityResponse: + api_key = os.getenv("OPENROUTER_API_KEY") + model = os.getenv("OPENROUTER_MODEL", "openai/gpt-oss-120b") + + if not api_key: + return AIConnectivityResponse( + ok=False, + message="OPENROUTER_API_KEY is not configured.", + model=model, + provider="openrouter", + ) + + try: + response = httpx.post( + "https://openrouter.ai/api/v1/chat/completions", + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + }, + json={ + "model": model, + "messages": [{"role": "user", "content": "Say OK in one word."}], + }, + timeout=20.0, + ) + response.raise_for_status() + payload = response.json() + content = payload.get("choices", [{}])[0].get("message", {}).get("content", "") + return AIConnectivityResponse( + ok=True, + message=content.strip() or "Connected", + model=model, + provider="openrouter", + ) + except Exception as exc: + return AIConnectivityResponse( + ok=False, + message=f"OpenRouter request failed: {exc}", + model=model, + provider="openrouter", + ) + + +@app.post("/api/ai/board-action", response_model=AIBoardActionResponse) +async def ai_board_action(request: AIBoardActionRequest) -> AIBoardActionResponse: + api_key = os.getenv("OPENROUTER_API_KEY") + model = os.getenv("OPENROUTER_MODEL", "openai/gpt-oss-120b") + + if not api_key: + return AIBoardActionResponse(reply="OpenRouter is not configured.", applied=False) + + try: + board = get_board_state(request.username) + except ValueError: + raise HTTPException(status_code=404, detail="User not found") from None + + history_text = "" + if request.history: + history_text = "\n".join( + f"{item.get('role', 'user')}: {item.get('content', '')}" for item in request.history + ) + + prompt = f""" +You are helping manage a Kanban board. +Board JSON: +{json.dumps(board, indent=2)} + +Conversation history: +{history_text} + +User request: +{request.question} + +Reply in JSON with this exact shape: +{{"reply": "short conversational response", "board_update": {{"columns": [...], "cards": {{...}}}}}} +If no board change is needed, set board_update to null. +""".strip() + + try: + response = httpx.post( + "https://openrouter.ai/api/v1/chat/completions", + headers={ + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + }, + json={ + "model": model, + "messages": [{"role": "user", "content": prompt}], + }, + timeout=20.0, + ) + response.raise_for_status() + payload = response.json() + message_content = ( + payload.get("choices", [{}])[0].get("message", {}).get("content", "") + ) + parsed = json.loads(message_content) + reply = parsed.get("reply", "I could not process that request.") + if not isinstance(parsed, dict): + return AIBoardActionResponse(reply=reply, applied=False) + + try: + ai_payload = AIBoardUpdatePayload.model_validate(parsed) + except Exception: + return AIBoardActionResponse(reply=reply, applied=False) + except Exception: + return AIBoardActionResponse(reply="I could not process that request.", applied=False) + + if not ai_payload.board_update: + return AIBoardActionResponse(reply=ai_payload.reply, applied=False) + + try: + updated_board = replace_board_state(request.username, ai_payload.board_update) + except ValueError as exc: + return AIBoardActionResponse(reply=f"I could not apply the update: {exc}", applied=False) + + return AIBoardActionResponse(reply=ai_payload.reply, applied=True, board=updated_board) + + +@app.get("/{full_path:path}", include_in_schema=False) +async def serve_frontend(full_path: str) -> FileResponse: + candidate_paths = [ + FRONTEND_BUILD_DIR / full_path, + FRONTEND_BUILD_DIR / f"{full_path}.html", + FRONTEND_BUILD_DIR / full_path / "index.html", + ] + + for candidate in candidate_paths: + if candidate.exists(): + return FileResponse(candidate) + + return FileResponse(FRONTEND_BUILD_DIR / "index.html") diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 00000000..0276dfbb --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "pm-backend" +version = "0.1.0" +requires-python = ">=3.11" +dependencies = [ + "fastapi>=0.115.0", + "uvicorn[standard]>=0.30.0", + "httpx>=0.28.0", +] diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt new file mode 100644 index 00000000..123ee1f2 --- /dev/null +++ b/backend/requirements-dev.txt @@ -0,0 +1,2 @@ +pytest>=8.0.0 +httpx>=0.27.0 diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 00000000..1b527310 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,3 @@ +fastapi>=0.115.0 +uvicorn[standard]>=0.30.0 +httpx>=0.28.0 diff --git a/backend/tests/test_ai_board_actions.py b/backend/tests/test_ai_board_actions.py new file mode 100644 index 00000000..f9d86a2a --- /dev/null +++ b/backend/tests/test_ai_board_actions.py @@ -0,0 +1,62 @@ +from unittest.mock import patch + +from fastapi.testclient import TestClient + +from backend.database import ensure_seed_data, init_db +from backend.main import app + +client = TestClient(app) + + +def setup_function() -> None: + init_db() + ensure_seed_data() + + +def test_ai_board_action_applies_valid_update() -> None: + with patch.dict("os.environ", {"OPENROUTER_API_KEY": "test-key"}), patch("httpx.post") as mocked_post: + mocked_post.return_value.raise_for_status.return_value = None + mocked_post.return_value.json.return_value = { + "choices": [ + { + "message": { + "content": '{"reply": "Updated the card title", "board_update": {"columns": [{"id": "col-1", "title": "Backlog", "cardIds": ["card-1"]}], "cards": {"card-1": {"id": "card-1", "title": "AI updated card", "details": "Updated by AI"}}}}' + } + } + ] + } + + response = client.post( + "/api/ai/board-action", + json={"username": "user", "question": "Rename the first card"}, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["applied"] is True + assert payload["reply"] == "Updated the card title" + assert payload["board"]["cards"]["card-1"]["title"] == "AI updated card" + + +def test_ai_board_action_rejects_invalid_update() -> None: + with patch.dict("os.environ", {"OPENROUTER_API_KEY": "test-key"}), patch("httpx.post") as mocked_post: + mocked_post.return_value.raise_for_status.return_value = None + mocked_post.return_value.json.return_value = { + "choices": [ + { + "message": { + "content": '{"reply": "I could not update the board", "board_update": {"foo": "bar"}}' + } + } + ] + } + + response = client.post( + "/api/ai/board-action", + json={"username": "user", "question": "Do something invalid"}, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["applied"] is False + assert payload["reply"] == "I could not update the board" diff --git a/backend/tests/test_ai_connectivity.py b/backend/tests/test_ai_connectivity.py new file mode 100644 index 00000000..a0516adf --- /dev/null +++ b/backend/tests/test_ai_connectivity.py @@ -0,0 +1,34 @@ +import os +from unittest.mock import patch + +from fastapi.testclient import TestClient + +from backend.main import app + +client = TestClient(app) + + +def test_ai_connectivity_reports_missing_key() -> None: + with patch.dict(os.environ, {"OPENROUTER_API_KEY": ""}, clear=False): + response = client.get("/api/ai/connectivity") + + assert response.status_code == 200 + payload = response.json() + assert payload["ok"] is False + assert "OPENROUTER_API_KEY" in payload["message"] + + +def test_ai_connectivity_reports_success() -> None: + with patch.dict(os.environ, {"OPENROUTER_API_KEY": "demo-key"}, clear=False): + with patch("httpx.post") as mocked_post: + mocked_post.return_value.raise_for_status.return_value = None + mocked_post.return_value.json.return_value = { + "choices": [{"message": {"content": "OK"}}] + } + + response = client.get("/api/ai/connectivity") + + assert response.status_code == 200 + payload = response.json() + assert payload["ok"] is True + assert payload["message"] == "OK" diff --git a/backend/tests/test_frontend_serving.py b/backend/tests/test_frontend_serving.py new file mode 100644 index 00000000..4b88f140 --- /dev/null +++ b/backend/tests/test_frontend_serving.py @@ -0,0 +1,14 @@ +from fastapi.testclient import TestClient + +from backend.main import app + + +client = TestClient(app) + + +def test_root_serves_frontend_index() -> None: + response = client.get("/") + + assert response.status_code == 200 + assert "text/html" in response.headers["content-type"] + assert "Kanban Studio" in response.text diff --git a/backend/tests/test_persistence.py b/backend/tests/test_persistence.py new file mode 100644 index 00000000..d0f35257 --- /dev/null +++ b/backend/tests/test_persistence.py @@ -0,0 +1,43 @@ +from fastapi.testclient import TestClient + +from backend.database import ensure_seed_data, init_db +from backend.main import app + + +client = TestClient(app) + + +def setup_function() -> None: + init_db() + ensure_seed_data() + + +def test_get_board_returns_seed_data() -> None: + response = client.get("/api/board/user") + + assert response.status_code == 200 + payload = response.json() + assert payload["columns"] + assert payload["cards"] + + +def test_put_board_persists_updates() -> None: + payload = { + "columns": [ + {"id": "col-1", "title": "Backlog", "cardIds": ["card-1"]}, + {"id": "col-2", "title": "Done", "cardIds": []}, + ], + "cards": { + "card-1": {"id": "card-1", "title": "Updated card", "details": "New details"} + }, + } + + response = client.put("/api/board/user", json=payload) + + assert response.status_code == 200 + saved = response.json() + assert saved["columns"][0]["title"] == "Backlog" + assert saved["cards"]["card-1"]["title"] == "Updated card" + + follow_up = client.get("/api/board/user") + assert follow_up.json()["cards"]["card-1"]["title"] == "Updated card" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..1a5fb709 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "8000:8000" + environment: + PYTHONUNBUFFERED: "1" diff --git a/docs/PLAN.md b/docs/PLAN.md index 974fc652..9e2202ac 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -1,37 +1,271 @@ -# High level steps for project +# Project implementation plan -Part 1: Plan +## Part 1: Planning and project context -Enrich this document to plan out each of these parts in detail, with substeps listed out as a checklist to be checked off by the agent, and with tests and success critieria for each. Also create an AGENTS.md file inside the frontend directory that describes the existing code there. Ensure the user checks and approves the plan. +### Objective -Part 2: Scaffolding +Finish the planning work for the MVP so that implementation can proceed safely and predictably. -Set up the Docker infrastructure, the backend in backend/ with FastAPI, and write the start and stop scripts in the scripts/ directory. This should serve example static HTML to confirm that a 'hello world' example works running locally and also make an API call. +### Checklist -Part 3: Add in Frontend +- [x] Expand this plan into a detailed milestone-based checklist. +- [x] Create a frontend agent guide in the frontend directory that explains the existing structure and conventions. +- [x] Keep the plan aligned with the project requirements in the root AGENTS file. +- [ ] Review the updated plan with the user and obtain approval before implementation starts. -Now update so that the frontend is statically built and served, so that the app has the demo Kanban board displayed at /. Comprehensive unit and integration tests. +### Tests and verification -Part 4: Add in a fake user sign in experience +- Confirm that the plan covers all major milestones from scaffolding through AI integration. +- Confirm that the frontend guide reflects the current codebase structure and testing setup. -Now update so that on first hitting /, you need to log in with dummy credentials ("user", "password") in order to see the Kanban, and you can log out. Comprehensive tests. +### Success criteria -Part 5: Database modeling +- The plan is detailed enough for the agent to execute work incrementally without guessing. +- The frontend guide is accurate for the current codebase and can be used as a reference during implementation. -Now propose a database schema for the Kanban, saving it as JSON. Document the database approach in docs/ and get user sign off. +--- -Part 6: Backend +## Part 2: Scaffolding -Now add API routes to allow the backend to read and change the Kanban for a given user; test this thoroughly with backend unit tests. The database should be created if it doesn't exist. +### Objective -Part 7: Frontend + Backend +Create the local development foundation for the app, including Docker support, a FastAPI backend, and startup scripts. -Now have the frontend actually use the backend API, so that the app is a proper persistent Kanban board. Test very throughly. +### Checklist -Part 8: AI connectivity +- [ ] Create the Dockerfile and any required docker-compose or docker-related configuration. +- [ ] Create the backend package structure under backend/ with FastAPI app entry points. +- [ ] Add a minimal health endpoint and a simple example route that returns a basic response. +- [ ] Add start and stop scripts in scripts/ for Windows, macOS, and Linux. +- [ ] Verify that the app can start locally and serve a simple hello-world response. +- [ ] Verify that the backend can respond to a simple API request. -Now allow the backend to make an AI call via OpenRouter. Test connectivity with a simple "2+2" test and ensure the AI call is working. +### Tests and verification -Part 9: Now extend the backend call so that it always calls the AI with the JSON of the Kanban board, plus the user's question (and conversation history). The AI should respond with Structured Outputs that includes the response to the user and optionaly an update to the Kanban. Test thoroughly. +- Run the app locally and confirm the root page responds. +- Call the API endpoint and confirm it returns the expected payload. +- Confirm the startup and shutdown scripts work in the intended environment. -Part 10: Now add a beautiful sidebar widget to the UI supporting full AI chat, and allowing the LLM (as it determines) to update the Kanban based on its Structured Outputs. If the AI updates the Kanban, then the UI should refresh automatically. \ No newline at end of file +### Success criteria + +- The project can be started locally with a single command. +- A simple static or text response is served successfully. +- The backend is reachable over HTTP and responds to a basic API call. + +--- + +## Part 3: Frontend integration + +### Objective + +Serve the existing Next.js frontend from the application stack so the Kanban board is shown at the root route. + +### Checklist + +- [ ] Ensure the Next.js frontend can be built and served by the app stack. +- [ ] Configure the backend to serve the built frontend assets at /. +- [ ] Verify that the demo Kanban board is visible when visiting the app root. +- [ ] Add or update unit tests for the main board rendering logic. +- [ ] Add or update integration tests for the homepage experience. + +### Tests and verification + +- Run frontend unit tests. +- Run frontend integration tests. +- Open the app locally and confirm the board renders. + +### Success criteria + +- The frontend is served from the application entry point. +- The Kanban board appears at /. +- The relevant tests pass and protect the expected UI behavior. + +--- + +## Part 4: Fake sign-in experience + +### Objective + +Add a simple authentication gate so the app requires a dummy login before showing the board. + +### Checklist + +- [ ] Add a sign-in screen that accepts the dummy credentials user and password. +- [ ] Require authentication before the Kanban board is shown. +- [ ] Add a logout flow that returns the user to the sign-in experience. +- [ ] Preserve the existing board experience after authentication. +- [ ] Add tests for successful login, failed login, and logout behavior. + +### Tests and verification + +- Verify that unauthenticated users cannot view the board. +- Verify that valid credentials allow access. +- Verify that logout returns the user to the sign-in screen. + +### Success criteria + +- The sign-in experience is functional and simple. +- The board is hidden until authentication succeeds. +- Tests cover the main authentication flows. + +--- + +## Part 5: Database modeling + +### Objective + +Define the data model for the Kanban board and document the storage approach before backend implementation. + +### Checklist + +- [x] Define a minimal schema for users, boards, columns, and cards. +- [x] Save the schema as JSON in the docs directory. +- [x] Document the database approach, including why SQLite is appropriate for the MVP. +- [ ] Present the proposed schema to the user and obtain approval before backend persistence work begins. + +### Tests and verification + +- Validate that the proposed schema can represent the required board structure. +- Confirm the schema supports future multi-user growth without over-engineering the MVP. + +### Success criteria + +- The schema is documented clearly and is suitable for the current MVP scope. +- The user has reviewed and approved the persistence approach. + +--- + +## Part 6: Backend persistence + +### Objective + +Add backend routes that can read and modify Kanban data for a signed-in user. + +### Checklist + +- [ ] Create the database file automatically if it does not exist. +- [ ] Implement data access functions for users, boards, columns, and cards. +- [ ] Add API routes to fetch the board state and update board contents. +- [ ] Ensure the backend handles the MVP single-board-per-user case cleanly. +- [ ] Add backend unit tests for create, read, update, and delete operations. + +### Tests and verification + +- Verify that the API returns the initial board shape for a valid user. +- Verify that board updates persist after repeated requests. +- Verify that the database file is created automatically on first use. + +### Success criteria + +- The backend can persist and retrieve board state reliably. +- Unit tests cover the primary data operations. +- The database setup is robust for local development. + +--- + +## Part 7: Frontend and backend integration + +### Objective + +Connect the frontend to the backend so the board becomes persistent instead of being purely local state. + +### Checklist + +- [ ] Replace local-only board state with API-backed loading and saving. +- [ ] Handle loading states and error states in the UI. +- [ ] Ensure drag-and-drop actions save to the backend. +- [ ] Ensure card creation, renaming, and deletion flow through the backend. +- [ ] Add thorough frontend and API integration tests. + +### Tests and verification + +- Verify that the UI loads board data from the backend. +- Verify that changes are persisted and reflected after refresh. +- Verify that errors during save operations are surfaced clearly. + +### Success criteria + +- The board is fully persistent for a signed-in user. +- The UI behaves correctly when the backend is available and when it is not. +- Integration coverage is strong enough to catch regressions. + +--- + +## Part 8: AI connectivity + +### Objective + +Enable the backend to connect to OpenRouter and verify that AI requests can succeed. + +### Checklist + +- [ ] Add the OpenRouter client configuration using the existing environment variable. +- [ ] Add a simple connectivity test endpoint or internal test path. +- [ ] Verify the backend can make a basic request such as a simple arithmetic prompt. +- [ ] Confirm the API key and model configuration are wired correctly. + +### Tests and verification + +- Run a simple AI request and confirm that a valid response is returned. +- Capture and inspect any configuration issues before proceeding. + +### Success criteria + +- The backend can call the AI provider successfully. +- The basic connectivity test passes reliably. + +--- + +## Part 9: Structured AI board actions + +### Objective + +Allow the AI to work with the Kanban board data in a structured way. + +### Checklist + +- [ ] Send the current board JSON, the user question, and relevant conversation history to the AI. +- [ ] Require the AI response to follow a structured output format. +- [ ] Support a response that includes both a conversational answer and an optional Kanban update. +- [ ] Apply valid board updates safely and only when the payload is well-formed. +- [ ] Add backend tests for both successful and invalid AI responses. + +### Tests and verification + +- Verify that a valid AI update changes the board as expected. +- Verify that malformed AI output is handled safely. +- Verify that the conversational response is preserved even when no board change is requested. + +### Success criteria + +- The AI can interpret board context and respond in a structured format. +- The app can safely apply AI-driven board changes. +- Tests cover both success and failure cases. + +--- + +## Part 10: AI sidebar experience + +### Objective + +Add a polished AI chat sidebar to the UI so the user can interact with the board naturally. + +### Checklist + +- [ ] Add a sidebar widget for chat input and message history. +- [ ] Send user messages to the backend and display the AI response in the UI. +- [ ] Apply AI-driven board updates automatically when the structured response includes them. +- [ ] Refresh the board view immediately after a successful update. +- [ ] Add UI tests for the chat experience and state refresh behavior. + +### Tests and verification + +- Verify that the sidebar renders correctly. +- Verify that user messages send and receive responses. +- Verify that board updates appear immediately after AI-driven changes. + +### Success criteria + +- The AI experience feels integrated into the Kanban workflow. +- The board refreshes correctly after AI changes. +- The new UI is covered by tests and is stable for the MVP. diff --git a/docs/database-approach.md b/docs/database-approach.md new file mode 100644 index 00000000..3b33afb6 --- /dev/null +++ b/docs/database-approach.md @@ -0,0 +1,32 @@ +# Database approach for the MVP + +## Summary + +The MVP uses SQLite for local persistence. This keeps the application simple and avoids introducing a separate database service during the initial build. + +## Why SQLite + +- The app runs locally in a container. +- The MVP only needs one board per signed-in user. +- SQLite is easy to set up and works well with a file-based local database. +- The schema is simple enough to evolve later if the product grows. + +## Proposed schema + +The schema is designed around four core entities: + +- users: stores the user identity for the MVP login flow. +- boards: stores the board container for a user. +- columns: stores the board columns and their order. +- cards: stores the cards and their current column assignment. + +## MVP assumptions + +- There is one board per user. +- Columns are fixed in the MVP but can be renamed by the user. +- Cards can be moved between columns. +- The app creates the database automatically if it does not exist. + +## Future direction + +This design is intentionally simple, but it can support future extension to multiple boards or more advanced collaborative features without a full rewrite. diff --git a/docs/database-schema.json b/docs/database-schema.json new file mode 100644 index 00000000..728fbea2 --- /dev/null +++ b/docs/database-schema.json @@ -0,0 +1,54 @@ +{ + "database": { + "name": "pm.sqlite", + "engine": "sqlite", + "notes": "A local SQLite database is sufficient for the MVP because the app runs locally and only needs a single board per user." + }, + "tables": { + "users": { + "description": "Stores the authenticated users for the MVP.", + "columns": [ + { "name": "id", "type": "INTEGER", "primaryKey": true }, + { "name": "username", "type": "TEXT", "unique": true, "nullable": false }, + { "name": "password_hash", "type": "TEXT", "nullable": false }, + { "name": "created_at", "type": "TEXT", "nullable": false } + ] + }, + "boards": { + "description": "Stores one board per user for the MVP.", + "columns": [ + { "name": "id", "type": "INTEGER", "primaryKey": true }, + { "name": "user_id", "type": "INTEGER", "nullable": false, "references": "users.id" }, + { "name": "title", "type": "TEXT", "nullable": false, "default": "Project Board" }, + { "name": "created_at", "type": "TEXT", "nullable": false } + ] + }, + "columns": { + "description": "Stores the fixed board columns and their order.", + "columns": [ + { "name": "id", "type": "INTEGER", "primaryKey": true }, + { "name": "board_id", "type": "INTEGER", "nullable": false, "references": "boards.id" }, + { "name": "position", "type": "INTEGER", "nullable": false }, + { "name": "title", "type": "TEXT", "nullable": false } + ] + }, + "cards": { + "description": "Stores cards and the column they currently belong to.", + "columns": [ + { "name": "id", "type": "INTEGER", "primaryKey": true }, + { "name": "board_id", "type": "INTEGER", "nullable": false, "references": "boards.id" }, + { "name": "column_id", "type": "INTEGER", "nullable": false, "references": "columns.id" }, + { "name": "position", "type": "INTEGER", "nullable": false }, + { "name": "title", "type": "TEXT", "nullable": false }, + { "name": "details", "type": "TEXT", "nullable": false, "default": "" }, + { "name": "created_at", "type": "TEXT", "nullable": false } + ] + } + }, + "relationships": [ + "One user has one board in the MVP.", + "One board has many columns.", + "One board has many cards.", + "Each card belongs to one column at a time." + ] +} diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md new file mode 100644 index 00000000..d23b0574 --- /dev/null +++ b/frontend/AGENTS.md @@ -0,0 +1,43 @@ +# Frontend agent guide + +## Purpose + +This directory contains the Next.js frontend for the project management MVP. It is currently a demo Kanban experience with local-only state and a test setup for unit and end-to-end coverage. + +## Current structure + +- app/: App router entry points. The main page renders the Kanban board. +- components/: UI components for the board, cards, columns, and forms. +- lib/: Shared board logic and initial data state. +- tests/: End-to-end tests for the user experience. +- src/test/: Additional test helpers or component-level test assets if present. + +## Key files + +- src/app/page.tsx: Root page entry that renders the board. +- src/components/KanbanBoard.tsx: Main board container with drag-and-drop behavior and board state. +- src/components/KanbanColumn.tsx: Column rendering and interactions. +- src/components/KanbanCard.tsx: Card details rendering. +- src/components/NewCardForm.tsx: Form for adding cards. +- src/lib/kanban.ts: Shared board data types, sample data, and card movement logic. + +## Current implementation notes + +- The board is currently a client-side demo with local React state. +- Drag-and-drop is implemented with dnd-kit. +- The app uses TypeScript and Tailwind-style utility classes. +- Tests are run with Vitest for unit tests and Playwright for end-to-end tests. + +## Working conventions + +- Keep changes simple and aligned with the MVP scope. +- Prefer incremental changes over broad rewrites. +- Preserve existing component boundaries where possible. +- Add or update tests when the UI behavior changes. +- Follow the repository’s guidance to investigate root causes before making fixes. + +## Important reminders + +- The frontend is not yet wired to the backend or database. +- Authentication and persistence are planned for later milestones. +- The current goal is to keep the existing board experience intact while later milestones add backend integration and AI features. diff --git a/frontend/next.config.ts b/frontend/next.config.ts index e9ffa308..a18b38eb 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,7 +1,8 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + output: "export", + trailingSlash: true, }; export default nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c3ef7b4c..71858bce 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -30,6 +30,7 @@ "eslint": "^9", "eslint-config-next": "16.1.6", "jsdom": "^27.0.1", + "playwright": "^1.61.1", "tailwindcss": "^4", "typescript": "^5", "vitest": "^3.2.4" @@ -125,13 +126,13 @@ "license": "MIT" }, "node_modules/@babel/code-frame": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", - "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -140,9 +141,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", - "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -150,21 +151,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", - "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -181,14 +182,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", - "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -198,14 +199,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -215,9 +216,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -225,29 +226,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -267,9 +268,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -277,9 +278,9 @@ } }, "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==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -287,9 +288,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -297,27 +298,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -369,33 +370,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", - "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.6", - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -403,14 +404,14 @@ } }, "node_modules/@babel/types": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -2058,6 +2059,38 @@ "node": ">=18" } }, + "node_modules/@playwright/test/node_modules/playwright": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.0.tgz", + "integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/@playwright/test/node_modules/playwright-core": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz", + "integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.53", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", @@ -2066,9 +2099,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz", - "integrity": "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", "cpu": [ "arm" ], @@ -2080,9 +2113,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.0.tgz", - "integrity": "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", "cpu": [ "arm64" ], @@ -2094,9 +2127,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz", - "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", "cpu": [ "arm64" ], @@ -2108,9 +2141,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz", - "integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", "cpu": [ "x64" ], @@ -2122,9 +2155,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.0.tgz", - "integrity": "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", "cpu": [ "arm64" ], @@ -2136,9 +2169,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.0.tgz", - "integrity": "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", "cpu": [ "x64" ], @@ -2150,13 +2183,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.0.tgz", - "integrity": "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2164,13 +2200,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.0.tgz", - "integrity": "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2178,13 +2217,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz", - "integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2192,13 +2234,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.0.tgz", - "integrity": "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2206,13 +2251,16 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.0.tgz", - "integrity": "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2220,13 +2268,16 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.0.tgz", - "integrity": "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2234,13 +2285,16 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.0.tgz", - "integrity": "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2248,13 +2302,16 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.0.tgz", - "integrity": "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2262,13 +2319,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.0.tgz", - "integrity": "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2276,13 +2336,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.0.tgz", - "integrity": "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2290,13 +2353,16 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.0.tgz", - "integrity": "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2304,13 +2370,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz", - "integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2318,13 +2387,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.0.tgz", - "integrity": "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2332,9 +2404,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.0.tgz", - "integrity": "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", "cpu": [ "x64" ], @@ -2346,9 +2418,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.0.tgz", - "integrity": "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", "cpu": [ "arm64" ], @@ -2360,9 +2432,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz", - "integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", "cpu": [ "arm64" ], @@ -2374,9 +2446,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.0.tgz", - "integrity": "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", "cpu": [ "ia32" ], @@ -2388,9 +2460,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.0.tgz", - "integrity": "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", "cpu": [ "x64" ], @@ -2402,9 +2474,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz", - "integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", "cpu": [ "x64" ], @@ -2886,9 +2958,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "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" }, @@ -3125,9 +3197,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -3135,13 +3207,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -3496,9 +3568,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", - "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.7.tgz", + "integrity": "sha512-NEGWJS2XNu2PfRLQwOO3CTKj1tTETxNBdk454vDxVBhxJYhPaA/eS0nAI0c+1El1P7a60z8+i+ZrQoGESweGKg==", "dev": true, "license": "MIT", "dependencies": { @@ -3520,8 +3592,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.2.4", - "vitest": "3.2.4" + "@vitest/browser": "3.2.7", + "vitest": "3.2.7" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -3530,15 +3602,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -3547,13 +3619,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.4", + "@vitest/spy": "3.2.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -3574,9 +3646,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", "dev": true, "license": "MIT", "dependencies": { @@ -3587,13 +3659,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.4", + "@vitest/utils": "3.2.7", "pathe": "^2.0.3", "strip-literal": "^3.0.0" }, @@ -3602,13 +3674,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", + "@vitest/pretty-format": "3.2.7", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -3617,9 +3689,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3630,13 +3702,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", + "@vitest/pretty-format": "3.2.7", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, @@ -3678,9 +3750,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -3987,12 +4059,15 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/bidi-js": { @@ -4006,9 +4081,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -4030,9 +4105,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", + "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", "dev": true, "funding": [ { @@ -4050,11 +4125,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001800", + "electron-to-chromium": "^1.5.387", + "node-releases": "^2.0.50", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -4134,9 +4209,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001766", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", - "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", + "version": "1.0.30001803", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", + "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", "funding": [ { "type": "opencollective", @@ -4543,9 +4618,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.279", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz", - "integrity": "sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg==", + "version": "1.5.389", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", "dev": true, "license": "ISC" }, @@ -5395,9 +5470,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -5438,7 +5513,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -5615,9 +5689,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -5625,13 +5699,13 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -6476,10 +6550,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -7063,9 +7147,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -7103,9 +7187,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "funding": [ { "type": "github", @@ -7225,11 +7309,14 @@ } }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "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" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/object-assign": { "version": "4.1.1", @@ -7530,9 +7617,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "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": { @@ -7543,13 +7630,13 @@ } }, "node_modules/playwright": { - "version": "1.58.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.0.tgz", - "integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==", - "devOptional": true, + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.58.0" + "playwright-core": "1.61.1" }, "bin": { "playwright": "cli.js" @@ -7562,10 +7649,10 @@ } }, "node_modules/playwright-core": { - "version": "1.58.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz", - "integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==", - "devOptional": true, + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -7585,9 +7672,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -7605,7 +7692,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -7863,13 +7950,13 @@ } }, "node_modules/rollup": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz", - "integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@types/estree": "1.0.9" }, "bin": { "rollup": "dist/bin/rollup" @@ -7879,31 +7966,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.0", - "@rollup/rollup-android-arm64": "4.57.0", - "@rollup/rollup-darwin-arm64": "4.57.0", - "@rollup/rollup-darwin-x64": "4.57.0", - "@rollup/rollup-freebsd-arm64": "4.57.0", - "@rollup/rollup-freebsd-x64": "4.57.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", - "@rollup/rollup-linux-arm-musleabihf": "4.57.0", - "@rollup/rollup-linux-arm64-gnu": "4.57.0", - "@rollup/rollup-linux-arm64-musl": "4.57.0", - "@rollup/rollup-linux-loong64-gnu": "4.57.0", - "@rollup/rollup-linux-loong64-musl": "4.57.0", - "@rollup/rollup-linux-ppc64-gnu": "4.57.0", - "@rollup/rollup-linux-ppc64-musl": "4.57.0", - "@rollup/rollup-linux-riscv64-gnu": "4.57.0", - "@rollup/rollup-linux-riscv64-musl": "4.57.0", - "@rollup/rollup-linux-s390x-gnu": "4.57.0", - "@rollup/rollup-linux-x64-gnu": "4.57.0", - "@rollup/rollup-linux-x64-musl": "4.57.0", - "@rollup/rollup-openbsd-x64": "4.57.0", - "@rollup/rollup-openharmony-arm64": "4.57.0", - "@rollup/rollup-win32-arm64-msvc": "4.57.0", - "@rollup/rollup-win32-ia32-msvc": "4.57.0", - "@rollup/rollup-win32-x64-gnu": "4.57.0", - "@rollup/rollup-win32-x64-msvc": "4.57.0", + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" } }, @@ -8658,9 +8745,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -8668,13 +8755,13 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -8733,9 +8820,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "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": { @@ -9111,13 +9198,13 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -9242,9 +9329,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "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": { @@ -9255,20 +9342,20 @@ } }, "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", @@ -9298,8 +9385,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", "happy-dom": "*", "jsdom": "*" }, @@ -9328,9 +9415,9 @@ } }, "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "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": { @@ -9619,9 +9706,9 @@ } }, "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "dev": true, "license": "MIT", "engines": { diff --git a/frontend/package.json b/frontend/package.json index e87f493d..09783c0f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,6 +36,7 @@ "eslint": "^9", "eslint-config-next": "16.1.6", "jsdom": "^27.0.1", + "playwright": "^1.61.1", "tailwindcss": "^4", "typescript": "^5", "vitest": "^3.2.4" diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index e7c512b8..b721894f 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,5 +1,5 @@ -import { KanbanBoard } from "@/components/KanbanBoard"; +import { AuthGate } from "@/components/AuthGate"; export default function Home() { - return ; + return ; } diff --git a/frontend/src/components/AIAssistantSidebar.tsx b/frontend/src/components/AIAssistantSidebar.tsx new file mode 100644 index 00000000..03e62032 --- /dev/null +++ b/frontend/src/components/AIAssistantSidebar.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { useState } from "react"; + +type Message = { + id: string; + role: "user" | "assistant"; + content: string; +}; + +type AIAssistantSidebarProps = { + username: string; + onBoardUpdated?: (board: { columns: Array<{ id: string; title: string; cardIds: string[] }>; cards: Record }) => void; +}; + +export const AIAssistantSidebar = ({ username, onBoardUpdated }: AIAssistantSidebarProps) => { + const [messages, setMessages] = useState([ + { + id: "welcome", + role: "assistant", + content: "Ask me to update the board, create tasks, or summarize the workflow.", + }, + ]); + const [draft, setDraft] = useState(""); + const [isSending, setIsSending] = useState(false); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + const trimmed = draft.trim(); + if (!trimmed) { + return; + } + + const userMessage: Message = { + id: crypto.randomUUID(), + role: "user", + content: trimmed, + }; + + setMessages((prev) => [...prev, userMessage]); + setDraft(""); + setIsSending(true); + + try { + const response = await fetch(`/api/ai/board-action`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username, + question: trimmed, + history: messages.map(({ role, content }) => ({ role, content })), + }), + }); + + if (!response.ok) { + throw new Error("Assistant request failed"); + } + + const payload = await response.json(); + const assistantMessage: Message = { + id: crypto.randomUUID(), + role: "assistant", + content: payload.reply || "I couldn't process that request.", + }; + setMessages((prev) => [...prev, assistantMessage]); + + if (payload.applied && payload.board) { + onBoardUpdated?.(payload.board); + } + } catch { + const assistantMessage: Message = { + id: crypto.randomUUID(), + role: "assistant", + content: "I couldn't reach the assistant right now. Please try again in a moment.", + }; + setMessages((prev) => [...prev, assistantMessage]); + } finally { + setIsSending(false); + } + }; + + return ( +