diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..7d53361 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,20 @@ +{ + // Camada de conveniência: anexar um editor (VS Code / Claude Code Remote) + // ao container workspace pela LAN. A stack continua headless — esta config + // NÃO é dona do ciclo de vida (shutdownAction: none), apenas se conecta ao + // mesmo docker-compose que roda como serviço. + "name": "squire-workspace", + "dockerComposeFile": "../deploy/docker-compose.yml", + "service": "workspace", + "workspaceFolder": "/home/ai-debian/projects", + "remoteUser": "ai-debian", + // Fechar o editor nunca derruba o orquestrador/agente/dashboard. + "shutdownAction": "none", + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python" + ] + } + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a376721 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,21 @@ +# Não levar para o build context / camada da imagem: +.venv/ +__pycache__/ +**/__pycache__/ +*.pyc +.git/ +.pytest_cache/ +.ruff_cache/ +node_modules/ +# .env contém segredo — NÃO embutir na imagem (inclui deploy/.env com o +# DASHBOARD_WRITE_TOKEN). Os valores vão via `environment:` do compose. +.env +**/.env +# deploy/ é infra de host (compose); não precisa estar na imagem. +deploy/ +# dashboard/ é sub-app Next.js, buildado pelo seu próprio serviço/Dockerfile. +# Excluir do contexto da imagem workspace (evita arrastar fonte + node_modules). +dashboard/ +# Estado local de testes e planos não pertencem à imagem. +*.bak +.claude/ diff --git a/.env.example b/.env.example index 68e1b6b..57ca8e7 100644 --- a/.env.example +++ b/.env.example @@ -1,33 +1,36 @@ # Squire — environment variables -# Copy to .env and fill in your values (never commit .env) +# Copy to .env and fill in your values (never commit .env). +# The `squire` bash wrapper sources .env automatically at startup. +# Use guarded exports so variables already exported in the shell win: +# export VAR="${VAR:-value}" -# Required: persistent state directory -SQUIRE_STATE_ROOT=/path/to/state +# Persistent state directory (default: /home/ai-debian/squire-state) +export SQUIRE_STATE_ROOT="${SQUIRE_STATE_ROOT:-/path/to/state}" -# LLM local (LiteLLM-compatible endpoint) -SQUIRE_LITELLM_URL=http://localhost:4000/v1 -SQUIRE_LITELLM_MODEL=your-model-name -SQUIRE_LITELLM_KEY=sk-local +# Local LLM (any OpenAI-compatible endpoint: LiteLLM, Ollama /v1, llama.cpp server) +export SQUIRE_LITELLM_URL="${SQUIRE_LITELLM_URL:-http://localhost:11434/v1}" +export SQUIRE_LITELLM_MODEL="${SQUIRE_LITELLM_MODEL:-your-model-name}" +export SQUIRE_LITELLM_KEY="${SQUIRE_LITELLM_KEY:-sk-local}" -# Coding backend: litellm | aider | opencode -SQUIRE_CODING_BACKEND=opencode +# Coding backend: litellm | opencode | crush +export SQUIRE_CODING_BACKEND="${SQUIRE_CODING_BACKEND:-opencode}" # Claude Code binary path -SQUIRE_CLAUDE_BIN=claude +export SQUIRE_CLAUDE_BIN="${SQUIRE_CLAUDE_BIN:-claude}" # Claude Code rate limiting -SQUIRE_CC_MAX_CALLS=10 -SQUIRE_CC_WINDOW_MIN=30 +export SQUIRE_CC_MAX_CALLS="${SQUIRE_CC_MAX_CALLS:-10}" +export SQUIRE_CC_WINDOW_MIN="${SQUIRE_CC_WINDOW_MIN:-30}" # Inner loop limits -SQUIRE_INNER_MAX_ATTEMPTS=10 -SQUIRE_INNER_TIMEOUT=1200 +export SQUIRE_INNER_MAX_ATTEMPTS="${SQUIRE_INNER_MAX_ATTEMPTS:-10}" +export SQUIRE_INNER_TIMEOUT="${SQUIRE_INNER_TIMEOUT:-1200}" # Homologation -SQUIRE_MAX_HOMOLOG=3 -SQUIRE_LOOP_DETECT=3 -SQUIRE_NO_PROGRESS=3 +export SQUIRE_MAX_HOMOLOG="${SQUIRE_MAX_HOMOLOG:-5}" +export SQUIRE_LOOP_DETECT="${SQUIRE_LOOP_DETECT:-3}" +export SQUIRE_NO_PROGRESS="${SQUIRE_NO_PROGRESS:-3}" # Session -SQUIRE_LOCK_TTL=60 -SQUIRE_HEARTBEAT=300 +export SQUIRE_LOCK_TTL="${SQUIRE_LOCK_TTL:-60}" +export SQUIRE_HEARTBEAT="${SQUIRE_HEARTBEAT:-300}" diff --git a/CLAUDE.md b/CLAUDE.md index 1431e7c..9f268c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ para executar projetos de software de forma semi-autônoma: A proporção-alvo é **30 chamadas locais para cada 1 do Claude Code**. -O primeiro projeto conduzido pelo orquestrador é o **Orchestrator Dashboard**: +O primeiro projeto conduzido pelo orquestrador é o **Squire Dashboard**: uma página Next.js que mostra o estado dos projetos em tempo real, lendo arquivos JSON do filesystem. É o projeto se observando nascer. @@ -33,13 +33,15 @@ arquivos JSON do filesystem. É o projeto se observando nascer. - É onde o Claude Code opera e onde o squire executa - Tem acesso ao filesystem do Unraid via mount -### LLM local — Qwen via LiteLLM -- **llama.cpp** roda no Zordon com o modelo `Qwen3.5-35B-A3B-Q4_K_M` -- **LiteLLM** é o API gateway: `http://192.168.50.24:4000/v1` -- **Model alias**: `journal-synth` (aponta pro Qwen) -- **API key**: `sk-local` (placeholder, LiteLLM local não exige auth real) -- **Flags do llama.cpp**: `-fa on -ctk q8_0 -ctv q8_0 -ngl all --reasoning-budget -1 --cache-reuse 256` -- **Performance**: ~124 tok/s com reasoning_content visível +### LLM local — Qwen via Ollama +- **Ollama** roda no Zordon servindo o modelo `journal-synth:latest` + (Qwen3.5-35B-A3B, IQ4_NL, `num_ctx 98304`) +- **Endpoint OpenAI-compatible**: `http://192.168.50.24:11434/v1` +- **API key**: `ollama` (placeholder, Ollama não exige auth) +- A configuração local fica em `.env` na raiz do repo (gitignored), que o + wrapper `squire` carrega automaticamente +- Histórico: antes era um gateway LiteLLM na porta 4000 sobre llama.cpp + (desativado em 2026-06) ### Filesystem de estado ``` @@ -49,7 +51,7 @@ arquivos JSON do filesystem. É o projeto se observando nascer. ├── alerts.json ← alertas ativos ├── global-stats.json ← métricas agregadas ├── projects/ -│ ├── orchestrator-dashboard/ ← projeto-piloto +│ ├── squire-dashboard/ ← projeto-piloto │ │ ├── project.json │ │ ├── tasks.json │ │ ├── history.json @@ -128,7 +130,7 @@ São duas interações diferentes com o Claude Code: 5 tentativas, me ajuda a desbloquear." Resultado: instruções que voltam pro LLM local como extra_instructions. -## Projeto-piloto: Orchestrator Dashboard +## Projeto-piloto: Squire Dashboard ### Stack - **Next.js** (App Router) + TypeScript + Tailwind CSS @@ -149,7 +151,7 @@ diretório local com dados de exemplo. Em produção, monta o volume `/mnt/user/data/squire/` (read-only). ### Tasks do projeto -Ver `projects/orchestrator-dashboard/tasks.json` para o backlog completo. +Ver `projects/squire-dashboard/tasks.json` para o backlog completo. ## Convenções @@ -180,6 +182,11 @@ Mapeamento feature → doc (use este atalho antes de editar): | Viking pattern (`docs/viking/`) | `docs/padrao-viking.md` | `docs/en/viking-pattern.md` | | Arquitetura / fluxo / componentes | `docs/arquitetura.md` | `docs/en/architecture.md` | | Failure mode novo / fix conhecido | `docs/troubleshooting.md` | `docs/en/troubleshooting.md` | +| Dashboard (UI Next.js em `dashboard/`) | `dashboard/CLAUDE.md` + `dashboard/README.md` (mesma fonte; sem mirror PT/EN) | + +> **Contrato JSON**: ao mudar um schema em `models.py`, atualize o tipo +> espelhado em `dashboard/src/lib/types.ts` **no mesmo commit** (o drift é +> coberto pelo teste `dashboard/src/lib/taskDefaults.test.ts`). Regras: @@ -231,8 +238,13 @@ Regras: - Não commitar diretamente na `main` — usar branches + merge ### Estrutura de diretórios (dashboard) + +O dashboard vive **dentro deste repo**, em `dashboard/` (sub-app: tem seu +próprio `package.json`/`tsconfig`/testes). Briefing próprio em +[`dashboard/CLAUDE.md`](dashboard/CLAUDE.md). + ``` -orchestrator-dashboard/ +dashboard/ ├── src/ │ ├── app/ │ │ ├── layout.tsx @@ -266,10 +278,10 @@ orchestrator-dashboard/ ### Squire (Python) ```bash -SQUIRE_STATE_ROOT=/mnt/user/data/squire -SQUIRE_LITELLM_URL=http://192.168.50.24:4000/v1 -SQUIRE_LITELLM_MODEL=journal-synth -SQUIRE_LITELLM_KEY=sk-local +SQUIRE_STATE_ROOT=/home/ai-debian/squire-state +SQUIRE_LITELLM_URL=http://192.168.50.24:11434/v1 +SQUIRE_LITELLM_MODEL=journal-synth:latest +SQUIRE_LITELLM_KEY=ollama SQUIRE_INNER_MAX_ATTEMPTS=10 SQUIRE_INNER_TIMEOUT=300 SQUIRE_CLAUDE_BIN=claude @@ -282,7 +294,7 @@ SQUIRE_HEARTBEAT=300 ### Dashboard (Next.js) ```bash -ORCHESTRATOR_DATA_PATH=/mnt/user/data/squire +SQUIRE_DATA_PATH=/home/ai-debian/squire-state NEXT_PUBLIC_REFRESH_INTERVAL=30000 ``` @@ -291,28 +303,53 @@ NEXT_PUBLIC_REFRESH_INTERVAL=30000 ### Squire ```bash cd /caminho/do/squire -python squire.py orchestrator-dashboard # execução normal -python squire.py orchestrator-dashboard --dry-run # simula sem executar -python squire.py orchestrator-dashboard --resume # retoma de crash +python squire.py squire-dashboard # execução normal +python squire.py squire-dashboard --dry-run # simula sem executar +python squire.py squire-dashboard --resume # retoma de crash ``` ### Dashboard (dev) ```bash -cd /caminho/do/orchestrator-dashboard +cd /caminho/do/squire/dashboard npm install npm run dev ``` ### Dashboard (produção) + +Canônico: a **stack** em `deploy/docker-compose.yml` sobe workspace + +dashboard juntos (ver "Stack em container" acima). O dashboard roda **na VM +Ai-Debian** (não no Unraid: o estado fica no disco local da VM). + ```bash -docker build -t orchestrator-dashboard . -# Pedir ao Danilo para criar o container no Unraid com: -# - Imagem: orchestrator-dashboard -# - Porta: 3100:3000 -# - Volume: /mnt/user/data/squire:/data:ro -# - Env: ORCHESTRATOR_DATA_PATH=/data +cd /home/ai-debian/squire +docker compose -f deploy/docker-compose.yml up -d --build +# Porta: 3101:3000 (3100 está ocupada pelo browserless na VM) +# Estado: volume nomeado squire-state em /data (rw — o dashboard escreve +# ack/dismiss de alertas via POST /api/alerts/ack) +# user: 1000:1000 (arquivos de estado são 0600 ai-debian) +# URL: http://:3101 ``` +O `dashboard/docker-compose.yml` (standalone, build context `.`) ainda serve +para rodar só o dashboard em dev, mas é **superseded** pela stack. + +### Stack em container (workspace + dashboard) + +Alternativa que isola o squire inteiro do host: stack docker-compose de +dois containers em `deploy/docker-compose.yml` — `workspace` (orquestrador ++ agente + sshd + toolchains + repos, SSH em `:2222`, supervisão s6-overlay +substituindo o unit systemd) e o `dashboard` (imagem existente, `:3101`). +Ambos compartilham o volume nomeado `squire-state` em `/data`. Detalhes, +recuperação de lock após restart e o seam de Docker-in-Docker em +[docs/estado-e-recuperacao.md](docs/estado-e-recuperacao.md). + +> **REGRA CRÍTICA (DinD)**: o `workspace` **não** tem Docker e **nunca** +> deve montar o socket do host/Unraid — isso daria controle do Docker do +> host ao container e anularia a contenção. Tasks que precisam buildar +> imagem são escaladas ao humano (alerta `requires_container_build`). A +> única evolução futura aceitável é DinD **rootless** dentro do container. + ## Notas para o Claude Code 1. **Você é o tech lead**. Planeja, revisa, e decide. O trabalho braçal de diff --git a/Dockerfile.workspace b/Dockerfile.workspace new file mode 100644 index 0000000..730d456 --- /dev/null +++ b/Dockerfile.workspace @@ -0,0 +1,107 @@ +# Dockerfile.workspace — container "workspace" do squire. +# +# Contém o orquestrador (squire.py), o agente da fila de comandos +# (agent_cli.py), sshd e os toolchains que o inner loop precisa +# (python, git, node, claude, opencode). É o container no qual o +# operador faz SSH; o dashboard roda em container separado e ambos +# compartilham o volume de estado. +# +# Base Debian bookworm (glibc): o binário opencode é glibc-prebuilt, +# então Alpine/musl está fora. +FROM python:3.11-slim-bookworm + +# ── Versões fixadas (reprodutibilidade) ───────────────────────────── +# claude: casa com o `claude --version` do host (2.1.179). +# node: 22.x para casar com o host onde os projetos foram criados. +ARG CLAUDE_VERSION=2.1.179 +ARG NODE_MAJOR=22 +ARG S6_OVERLAY_VERSION=3.2.0.2 +# Fixado na versão do host (opencode --version). Deixe vazio para "latest". +ARG OPENCODE_VERSION=1.15.13 + +ENV DEBIAN_FRONTEND=noninteractive \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +# ── Pacotes de sistema (uma camada) ───────────────────────────────── +# procps → ps (squire/doctor usam); xz-utils → extrair s6; openssh-server +# → acesso externo; build-essential FICA DE FORA (adicione via projeto +# se um npm module nativo bloquear). +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + openssh-server \ + ca-certificates \ + curl \ + xz-utils \ + procps \ + sudo \ + less \ + nano \ + && curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# ── s6-overlay (PID 1: reaping de zumbis + supervisão de serviços) ── +ADD "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" /tmp/s6-noarch.tar.xz +ADD "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-x86_64.tar.xz" /tmp/s6-x86_64.tar.xz +RUN tar -C / -Jxpf /tmp/s6-noarch.tar.xz \ + && tar -C / -Jxpf /tmp/s6-x86_64.tar.xz \ + && rm -f /tmp/s6-noarch.tar.xz /tmp/s6-x86_64.tar.xz + +# ── claude (homologação/escalação) ────────────────────────────────── +# Instalado via npm global e fixado; o homologator parseia o +# `claude --print --output-format json`, então a versão importa. +RUN npm install -g "@anthropic-ai/claude-code@${CLAUDE_VERSION}" \ + && npm cache clean --force + +# ── Usuário ai-debian (uid/gid 1000) ──────────────────────────────── +# Casa com os arquivos de estado (0600 uid 1000) e com o user 1000:1000 +# do container do dashboard. +RUN groupadd -g 1000 ai-debian \ + && useradd -m -u 1000 -g 1000 -s /bin/bash ai-debian \ + && mkdir -p /home/ai-debian/.ssh /home/ai-debian/projects \ + && chown -R ai-debian:ai-debian /home/ai-debian \ + && chmod 700 /home/ai-debian/.ssh + +# ── opencode (backend de coding padrão) ───────────────────────────── +# Instalado como ai-debian no ~/.opencode/bin (instalador oficial). +# VERSION="" → instala latest; senão fixa a release (o instalador trata +# "latest" como a tag literal vlatest, que não existe). +USER ai-debian +RUN if [ -n "${OPENCODE_VERSION}" ]; then \ + curl -fsSL https://opencode.ai/install | VERSION="${OPENCODE_VERSION}" bash; \ + else \ + curl -fsSL https://opencode.ai/install | bash; \ + fi +USER root + +# ── Código do squire + venv ───────────────────────────────────────── +# Copiamos o repo (self-contained: doctor passa no primeiro boot). O +# compose pode sobrepor com um bind-mount do repo para dev ao vivo. +COPY --chown=ai-debian:ai-debian . /home/ai-debian/squire +RUN python -m venv /home/ai-debian/squire/.venv \ + && /home/ai-debian/squire/.venv/bin/pip install --no-cache-dir -e /home/ai-debian/squire \ + && chown -R ai-debian:ai-debian /home/ai-debian/squire/.venv \ + && ln -sf /home/ai-debian/squire/squire /usr/local/bin/squire + +# ── sshd: só chave pública, sem root, sem senha ───────────────────── +RUN mkdir -p /run/sshd \ + && sed -i \ + -e 's/^#\?PermitRootLogin.*/PermitRootLogin no/' \ + -e 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' \ + -e 's/^#\?PubkeyAuthentication.*/PubkeyAuthentication yes/' \ + /etc/ssh/sshd_config \ + && printf '\nAllowUsers ai-debian\n' >> /etc/ssh/sshd_config + +# ── Init scripts (cont-init.d, oneshot, root) + serviços (services.d) +COPY docker/cont-init.d/ /etc/cont-init.d/ +COPY docker/services.d/ /etc/services.d/ +COPY docker/profile.d/squire.sh /etc/profile.d/squire.sh +RUN chmod +x /etc/cont-init.d/* /etc/services.d/*/run /etc/profile.d/squire.sh + +# Porta SSH (mapeada para 2222 no host pelo compose). +EXPOSE 22 + +# s6 inicia tudo: cont-init.d (chown/ssh-keygen) → services.d (sshd, agent). +ENTRYPOINT ["/init"] diff --git a/README.en.md b/README.en.md index bf9a8bd..3e11492 100644 --- a/README.en.md +++ b/README.en.md @@ -98,6 +98,10 @@ squire/ ├── progress.py ← progress.txt generator ├── tasks_cli.py ← `squire tasks` subcommands ├── tests/ ← pytest +├── dashboard/ ← Next.js app (read-write state UI) — sub-app +├── deploy/ ← docker-compose stack (workspace + dashboard) +├── docker/ ← s6 init/services for the workspace container +├── Dockerfile.workspace ← orchestrator image (SSH, toolchains) └── docs/ ← documentation └── en/ ← English mirror (this language) ``` diff --git a/README.md b/README.md index 93f8a21..5b59feb 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,10 @@ squire/ ├── progress.py ← geração de progress.txt ├── tasks_cli.py ← subcomandos `squire tasks` ├── tests/ ← pytest +├── dashboard/ ← app Next.js (UI read-write do estado) — sub-app +├── deploy/ ← stack docker-compose (workspace + dashboard) +├── docker/ ← init/serviços s6 do container workspace +├── Dockerfile.workspace ← imagem do orquestrador (SSH, toolchains) └── docs/ ← documentação (esta pasta) └── en/ ← mirror em inglês ``` diff --git a/accounting.py b/accounting.py new file mode 100644 index 0000000..49104dc --- /dev/null +++ b/accounting.py @@ -0,0 +1,59 @@ +""" +Contabilidade de uso/custo compartilhada entre o orquestrador (squire.py) +e o ciclo de fix (fix_cli.py). Mantém GlobalStats e Task.cost_usd em +sincronia com cada chamada de LLM/Claude realizada. +""" + +from __future__ import annotations + +from typing import Optional + +from models import GlobalStats, Task, TokenUsage + + +def record_usage( + stats: GlobalStats, + usage: Optional[TokenUsage], + task: Optional[Task] = None, + cc_call: bool = True, +) -> float: + """Aplica uma chamada já realizada em GlobalStats (in-place) e na task. + + Retorna o custo registrado. cc_call=True identifica chamadas ao Claude + Code (contam em daily_calls_unknown_cost quando a usage não veio). + O caller persiste stats (save_stats) quando fizer sentido. + """ + if usage is None: + if cc_call: + stats.daily_calls_unknown_cost += 1 + return 0.0 + + cost = max(0.0, float(usage.cost_usd or 0.0)) + tokens = int(usage.prompt_tokens or 0) + int(usage.completion_tokens or 0) + stats.cost_estimate_usd += cost + stats.daily_tokens += tokens + if usage.tokens_unknown and cc_call: + stats.daily_calls_unknown_cost += 1 + if usage.model: + stats.cost_by_model[usage.model] = ( + stats.cost_by_model.get(usage.model, 0.0) + cost + ) + if task is not None: + task.cost_usd = float(task.cost_usd or 0.0) + cost + return cost + + +def record_homologated(stats: GlobalStats, first_try: bool) -> None: + """Atualiza os contadores da taxa de aprovação após um veredito final. + + Usado pelo loop do orquestrador e pelo `squire fix` (que nunca é + first-try — a task já queimou rodadas antes de bloquear). + """ + stats.tasks_homologated_today += 1 + if first_try: + stats.tasks_approved_first_try_today += 1 + stats.approval_first_try_rate = round( + 100.0 * stats.tasks_approved_first_try_today + / max(1, stats.tasks_homologated_today), + 1, + ) diff --git a/agent_cli.py b/agent_cli.py new file mode 100644 index 0000000..b5ee292 --- /dev/null +++ b/agent_cli.py @@ -0,0 +1,348 @@ +#!/usr/bin/env python3 +""" +Agente host do squire — executa comandos enfileirados pelo dashboard. +Invocado por 'squire agent [--once] [--poll N]'. + +O dashboard (container, sem acesso ao host) escreve comandos em +$SQUIRE_STATE_ROOT/commands/pending/.json. Este agente roda na VM, +reivindica cada comando via rename atômico para running/, executa o CLI +correspondente com argv em lista (nunca shell) e grava o resultado em +done/.json. Apenas os tipos da whitelist CommandType são aceitos, +com validação estrita de project_id/args. +""" + +from __future__ import annotations + +import os +import re +import subprocess +import sys +import time +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Optional + +import checkpoint as ckpt +import config +from models import CommandResult, CommandStatus, CommandType, QueuedCommand + +# ── Cores ────────────────────────────────────────────────────────── + +RED = "\033[0;31m" +GREEN = "\033[0;32m" +YELLOW = "\033[1;33m" +CYAN = "\033[0;36m" +BOLD = "\033[1m" +RESET = "\033[0m" + +SQUIRE_DIR = Path(__file__).resolve().parent +WRAPPER = SQUIRE_DIR / "squire" +VENV_PY = SQUIRE_DIR / ".venv" / "bin" / "python" + +_PROJECT_ID_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,63}$") +_TASK_ID_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,63}$") +_TAIL_BYTES = 8 * 1024 + + +def _log(msg: str, kind: str = "info") -> None: + icon = {"info": f"{CYAN}→{RESET}", "ok": f"{GREEN}✓{RESET}", + "warn": f"{YELLOW}⚠{RESET}", "error": f"{RED}✗{RESET}"}[kind] + ts = datetime.now().strftime("%H:%M:%S") + print(f"[{ts}] {icon} {msg}", flush=True) + + +# ── Filesystem da fila ───────────────────────────────────────────── + +def ensure_queue_dirs() -> None: + for d in (config.COMMANDS_PENDING, config.COMMANDS_RUNNING, config.COMMANDS_DONE): + d.mkdir(parents=True, exist_ok=True) + + +def recover_orphans() -> int: + """Comandos órfãos em running/ (agente morreu no meio) viram failed. + + Nunca re-executa — um 'squire run' duplicado seria pior que pedir + ao usuário para clicar de novo. + """ + count = 0 + for path in sorted(config.COMMANDS_RUNNING.glob("*.json")): + try: + cmd = QueuedCommand.model_validate_json(path.read_text()) + result = CommandResult( + id=cmd.id, type=cmd.type, project_id=cmd.project_id, + status=CommandStatus.failed, + error="agente reiniciado durante a execução — repita o comando", + finished_at=datetime.now(timezone.utc), + ) + ckpt.save_model(config.COMMANDS_DONE / path.name, result) + except Exception: + pass # arquivo ilegível — só remove + path.unlink(missing_ok=True) + count += 1 + return count + + +def cleanup_done() -> int: + """Apaga resultados em done/ mais velhos que o TTL.""" + cutoff = time.time() - config.COMMAND_RESULT_TTL_HOURS * 3600 + count = 0 + for path in config.COMMANDS_DONE.glob("*.json"): + try: + if path.stat().st_mtime < cutoff: + path.unlink() + count += 1 + except FileNotFoundError: + pass + return count + + +def claim_next() -> Optional[Path]: + """Reivindica o pending mais antigo movendo-o (atômico) para running/.""" + candidates = sorted( + config.COMMANDS_PENDING.glob("*.json"), key=lambda p: p.stat().st_mtime + ) + for path in candidates: + target = config.COMMANDS_RUNNING / path.name + try: + os.rename(path, target) + return target + except FileNotFoundError: + continue # outro processo levou — segue + return None + + +# ── Validação + montagem de argv ─────────────────────────────────── + +def validate(cmd: QueuedCommand) -> Optional[str]: + """Retorna mensagem de erro, ou None se o comando é válido.""" + needs_project = cmd.type != CommandType.kill + if needs_project: + if not cmd.project_id or not _PROJECT_ID_RE.match(cmd.project_id): + return f"project_id inválido: {cmd.project_id!r}" + project_dir = (config.PROJECTS_DIR / cmd.project_id).resolve() + if not str(project_dir).startswith(str(config.PROJECTS_DIR.resolve()) + os.sep): + return f"project_id fora de projects/: {cmd.project_id!r}" + exists = project_dir.exists() + if cmd.type == CommandType.new_project: + if exists: + return f"projeto '{cmd.project_id}' já existe" + elif not exists: + return f"projeto '{cmd.project_id}' não existe" + + if cmd.type == CommandType.new_project: + backend = cmd.args.get("backend", "opencode") + if backend not in ("opencode", "litellm", "crush"): + return f"backend inválido: {backend!r}" + repo_path = cmd.args.get("repo_path") + if repo_path is not None: + rp = Path(repo_path) + if not rp.is_absolute(): + return f"repo_path deve ser absoluto: {repo_path!r}" + root = config.AGENT_REPO_ROOT.resolve() + if not str(rp.resolve()).startswith(str(root) + os.sep): + return f"repo_path fora de {root}: {repo_path!r}" + + if cmd.type == CommandType.plan_tasks: + mode = cmd.args.get("mode", "append") + if mode not in ("append", "replace"): + return f"mode inválido: {mode!r}" + + if cmd.type in (CommandType.split_task, CommandType.fix_task): + task_id = cmd.args.get("task_id", "") + if not _TASK_ID_RE.match(task_id): + return f"task_id inválido: {task_id!r}" + + return None + + +def build_argv(cmd: QueuedCommand) -> list[str]: + """Monta o argv (lista, nunca shell) para o comando validado.""" + if cmd.type == CommandType.new_project: + argv = [str(WRAPPER), "new", cmd.project_id, "--yes"] + if cmd.args.get("name"): + argv += ["--name", str(cmd.args["name"])] + if cmd.args.get("repo_path"): + argv += ["--repo", str(cmd.args["repo_path"])] + if cmd.args.get("stack"): + argv += ["--stack", str(cmd.args["stack"])] + argv += ["--backend", str(cmd.args.get("backend", "opencode"))] + if cmd.args.get("git_init", True): + argv += ["--git-init"] + return argv + + if cmd.type == CommandType.run: + return [str(WRAPPER), "bg", cmd.project_id] + + if cmd.type == CommandType.resume: + return [str(WRAPPER), "resume", cmd.project_id, "bg"] + + if cmd.type == CommandType.kill: + return [str(WRAPPER), "kill"] + + if cmd.type == CommandType.plan_tasks: + argv = [str(VENV_PY), "-u", str(SQUIRE_DIR / "tasks_cli.py"), + "plan", cmd.project_id, "--yes", + "--mode", str(cmd.args.get("mode", "append"))] + if cmd.args.get("description"): + argv += ["--desc", str(cmd.args["description"])] + return argv + + if cmd.type == CommandType.split_task: + return [str(VENV_PY), "-u", str(SQUIRE_DIR / "tasks_cli.py"), + "split", cmd.project_id, str(cmd.args["task_id"]), "--yes"] + + if cmd.type == CommandType.fix_task: + return [str(VENV_PY), "-u", str(SQUIRE_DIR / "fix_cli.py"), + cmd.project_id, str(cmd.args["task_id"]), "--yes"] + + raise ValueError(f"tipo não suportado: {cmd.type}") + + +# ── Execução ─────────────────────────────────────────────────────── + +def _tail(text: str) -> str: + return text[-_TAIL_BYTES:] if text else "" + + +def execute(running_path: Path) -> CommandResult: + """Executa o comando reivindicado e grava o resultado em done/.""" + started = datetime.now(timezone.utc) + try: + cmd = QueuedCommand.model_validate_json(running_path.read_text()) + except Exception as e: + result = CommandResult( + id=running_path.stem, type=CommandType.kill, status=CommandStatus.failed, + error=f"JSON de comando inválido: {e}", + started_at=started, finished_at=datetime.now(timezone.utc), + ) + ckpt.save_model(config.COMMANDS_DONE / running_path.name, result) + running_path.unlink(missing_ok=True) + return result + + problem = validate(cmd) + if problem is None: + argv = build_argv(cmd) + _log(f"executando {cmd.type.value} ({cmd.project_id or '-'}): {' '.join(argv[:4])}…") + env = {**os.environ, "SQUIRE_STATE_ROOT": str(config.STATE_ROOT)} + try: + proc = subprocess.run( + argv, capture_output=True, text=True, + timeout=config.COMMAND_TIMEOUT_SECONDS, env=env, + ) + status = CommandStatus.done if proc.returncode == 0 else CommandStatus.failed + result = CommandResult( + id=cmd.id, type=cmd.type, project_id=cmd.project_id, + status=status, exit_code=proc.returncode, + stdout_tail=_tail(proc.stdout), stderr_tail=_tail(proc.stderr), + started_at=started, finished_at=datetime.now(timezone.utc), + error=None if status == CommandStatus.done else f"exit code {proc.returncode}", + ) + except subprocess.TimeoutExpired: + result = CommandResult( + id=cmd.id, type=cmd.type, project_id=cmd.project_id, + status=CommandStatus.failed, + error=f"timeout após {config.COMMAND_TIMEOUT_SECONDS}s", + started_at=started, finished_at=datetime.now(timezone.utc), + ) + else: + _log(f"comando rejeitado: {problem}", "warn") + result = CommandResult( + id=cmd.id, type=cmd.type, project_id=cmd.project_id, + status=CommandStatus.failed, error=problem, + started_at=started, finished_at=datetime.now(timezone.utc), + ) + + ckpt.save_model(config.COMMANDS_DONE / running_path.name, result) + running_path.unlink(missing_ok=True) + kind = "ok" if result.status == CommandStatus.done else "error" + _log(f"{cmd.type.value} → {result.status.value}", kind) + return result + + +# ── Pidfile (instância única) ────────────────────────────────────── + +def _acquire_pidfile() -> bool: + pidfile = config.COMMANDS_DIR / "agent.pid" + if pidfile.exists(): + try: + pid = int(pidfile.read_text().strip()) + os.kill(pid, 0) + _log(f"outro agente já roda (pid {pid})", "error") + return False + except (ValueError, ProcessLookupError, PermissionError): + pass # stale + pidfile.write_text(str(os.getpid())) + return True + + +def _release_pidfile() -> None: + pidfile = config.COMMANDS_DIR / "agent.pid" + try: + if pidfile.exists() and int(pidfile.read_text().strip()) == os.getpid(): + pidfile.unlink() + except (ValueError, OSError): + pass + + +# ── Main loop ────────────────────────────────────────────────────── + +def run_agent(once: bool = False, poll_seconds: Optional[float] = None) -> int: + poll = poll_seconds if poll_seconds is not None else config.AGENT_POLL_SECONDS + ensure_queue_dirs() + if not _acquire_pidfile(): + return 1 + + try: + orphans = recover_orphans() + if orphans: + _log(f"{orphans} comando(s) órfão(s) marcados como failed", "warn") + cleanup_done() + _log(f"agente ativo — fila em {config.COMMANDS_DIR} (poll {poll}s)") + + last_cleanup = time.time() + while True: + claimed = claim_next() + if claimed is not None: + execute(claimed) + continue # drena a fila antes de dormir + if once: + return 0 + time.sleep(poll) + if time.time() - last_cleanup > 600: + cleanup_done() + last_cleanup = time.time() + except KeyboardInterrupt: + _log("agente encerrado (Ctrl+C)") + return 0 + finally: + _release_pidfile() + + +def main() -> None: + sys.path.insert(0, str(SQUIRE_DIR)) + args = sys.argv[1:] + if any(a in ("-h", "--help", "help") for a in args): + print(f""" +{BOLD}squire agent{RESET} — executa comandos enfileirados pelo dashboard + + {CYAN}squire agent{RESET} Loop contínuo (use com systemd --user) + {CYAN}squire agent --once{RESET} Processa a fila e sai (testes/cron) + {CYAN}squire agent --poll N{RESET} Intervalo de polling em segundos + +Fila: $SQUIRE_STATE_ROOT/commands/{{pending,running,done}}/ +Tipos aceitos: new_project, run, resume, kill, plan_tasks, split_task, fix_task +""") + return + once = "--once" in args + poll = None + if "--poll" in args: + try: + poll = float(args[args.index("--poll") + 1]) + except (IndexError, ValueError): + print(f"{RED}✗{RESET} --poll requer um número de segundos", file=sys.stderr) + sys.exit(1) + sys.exit(run_agent(once=once, poll_seconds=poll)) + + +if __name__ == "__main__": + main() diff --git a/alerts_cli.py b/alerts_cli.py new file mode 100644 index 0000000..676ee86 --- /dev/null +++ b/alerts_cli.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +""" +CLI para gerenciamento de alertas do squire. +Invocado por 'squire alerts [args]'. + +Os índices exibidos por 'alerts list' referem-se à posição do alerta na +lista de NÃO-reconhecidos (ordem do arquivo), e são o que 'ack'/'rm' +aceitam. O dashboard é um segundo escritor de alerts.json — em ambientes +com o dashboard ativo, prefira os seletores --project/--task, que não +sofrem corrida de índice. +""" + +from __future__ import annotations + +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Optional + +import checkpoint as ckpt +import config +from models import Alert, AlertList, AlertSeverity + +# ── Cores ────────────────────────────────────────────────────────── + +RED = "\033[0;31m" +GREEN = "\033[0;32m" +YELLOW = "\033[1;33m" +CYAN = "\033[0;36m" +BOLD = "\033[1m" +DIM = "\033[2m" +RESET = "\033[0m" + + +def _c(color: str, text: str) -> str: + return f"{color}{text}{RESET}" + + +SEVERITY_LABEL = { + AlertSeverity.critical: _c(RED, "CRIT"), + AlertSeverity.warning: _c(YELLOW, "WARN"), +} + + +# ── Helpers ──────────────────────────────────────────────────────── + +def _age(created_at: datetime) -> str: + """Idade compacta: 12min, 5h, 3d.""" + now = datetime.now(timezone.utc) + if created_at.tzinfo is None: + created_at = created_at.replace(tzinfo=timezone.utc) + delta = now - created_at + minutes = int(delta.total_seconds() // 60) + if minutes < 60: + return f"{minutes}min" + hours = minutes // 60 + if hours < 48: + return f"{hours}h" + return f"{delta.days}d" + + +def _indexed_unacked(alerts: AlertList) -> list[tuple[int, Alert]]: + """Pares (índice 1-based, alerta) dos não-reconhecidos, na ordem do arquivo.""" + return [ + (i, a) + for i, a in enumerate( + (a for a in alerts.alerts if not a.acknowledged), start=1 + ) + ] + + +def _format_line(idx: Optional[int], alert: Alert, dim: bool = False) -> str: + idx_str = f"{idx:3}" if idx is not None else " —" + task = alert.task_id or "—" + msg = alert.message.replace("\n", " ") + if len(msg) > 70: + msg = msg[:67] + "..." + line = ( + f"{idx_str} {SEVERITY_LABEL[alert.severity]} " + f"{alert.project_id}/{task} {_c(DIM, _age(alert.created_at))} " + f"{alert.type}: {msg}" + ) + if dim: + return f"{DIM}{line}{RESET}" + return line + + +def _resolve_indexes(alerts: AlertList, raw: list[str]) -> list[Alert]: + """Resolve índices 1-based contra a lista de não-reconhecidos.""" + indexed = dict(_indexed_unacked(alerts)) + selected: list[Alert] = [] + for token in raw: + try: + idx = int(token) + except ValueError: + print(f"{RED}✗{RESET} Índice inválido: '{token}'", file=sys.stderr) + sys.exit(1) + if idx not in indexed: + print( + f"{RED}✗{RESET} Índice {idx} fora do alcance " + f"(há {len(indexed)} alertas não-reconhecidos — veja 'squire alerts list').", + file=sys.stderr, + ) + sys.exit(1) + selected.append(indexed[idx]) + return selected + + +def _save(alerts: AlertList) -> None: + ckpt.save_model(config.ALERTS_FILE, alerts) + + +# ── Comandos ─────────────────────────────────────────────────────── + +def cmd_list(show_all: bool = False, project: Optional[str] = None) -> None: + alerts = ckpt.load_alerts() + indexed = _indexed_unacked(alerts) + + visible = [ + (idx, a) for idx, a in indexed + if project is None or a.project_id == project + ] + acked = [ + a for a in alerts.alerts + if a.acknowledged and (project is None or a.project_id == project) + ] + + if not visible and not (show_all and acked): + scope = f" do projeto '{project}'" if project else "" + print(f"{GREEN}✓{RESET} Nenhum alerta pendente{scope}.") + return + + if visible: + print(f"{BOLD}Alertas pendentes ({len(visible)}):{RESET}") + for idx, alert in visible: + print(_format_line(idx, alert)) + if show_all and acked: + print(f"\n{BOLD}Reconhecidos ({len(acked)}):{RESET}") + for alert in acked: + print(_format_line(None, alert, dim=True)) + if visible: + print(f"\n{DIM}Use 'squire alerts ack ' ou 'squire alerts ack --all'.{RESET}") + + +def cmd_ack( + indexes: list[str], + ack_all: bool = False, + project: Optional[str] = None, + task: Optional[str] = None, +) -> None: + alerts = ckpt.load_alerts() + + if indexes: + targets = _resolve_indexes(alerts, indexes) + elif ack_all or project or task: + targets = [ + a for a in alerts.alerts + if not a.acknowledged + and (project is None or a.project_id == project) + and (task is None or a.task_id == task) + ] + else: + print( + f"{RED}✗{RESET} Uso: alerts ack [...] | --all [--project ] [--task ]", + file=sys.stderr, + ) + sys.exit(1) + + if not targets: + print("Nenhum alerta correspondente.") + return + + for alert in targets: + alert.acknowledged = True + _save(alerts) + print(f"{GREEN}✓{RESET} {len(targets)} alerta(s) reconhecido(s).") + + +def cmd_rm( + indexes: list[str], + rm_acked: bool = False, + rm_all: bool = False, +) -> None: + alerts = ckpt.load_alerts() + + if indexes: + targets = _resolve_indexes(alerts, indexes) + elif rm_acked: + targets = [a for a in alerts.alerts if a.acknowledged] + elif rm_all: + targets = list(alerts.alerts) + else: + print( + f"{RED}✗{RESET} Uso: alerts rm [...] | --acked | --all", + file=sys.stderr, + ) + sys.exit(1) + + if not targets: + print("Nenhum alerta correspondente.") + return + + target_ids = {id(a) for a in targets} + alerts.alerts = [a for a in alerts.alerts if id(a) not in target_ids] + _save(alerts) + print(f"{GREEN}✓{RESET} {len(targets)} alerta(s) removido(s).") + + +# ── Main ─────────────────────────────────────────────────────────── + +def main() -> None: + sys.path.insert(0, str(Path(__file__).parent)) + + if len(sys.argv) < 2: + cmd_list() + return + + subcmd = sys.argv[1] + args = sys.argv[2:] + + if subcmd in ("-h", "--help", "help"): + _print_help() + return + + if subcmd == "list": + cmd_list(show_all="--all" in args, project=_get_flag(args, "--project")) + + elif subcmd == "ack": + indexes = [a for a in args if not a.startswith("--") and not _is_flag_value(args, a)] + cmd_ack( + indexes, + ack_all="--all" in args, + project=_get_flag(args, "--project"), + task=_get_flag(args, "--task"), + ) + + elif subcmd == "rm": + indexes = [a for a in args if not a.startswith("--")] + cmd_rm(indexes, rm_acked="--acked" in args, rm_all="--all" in args) + + else: + print(f"{RED}✗{RESET} Subcomando desconhecido: '{subcmd}'", file=sys.stderr) + _print_help() + sys.exit(1) + + +def _get_flag(args: list[str], flag: str) -> Optional[str]: + """Extrai o valor de uma flag --key value dos args.""" + try: + idx = args.index(flag) + if idx + 1 < len(args): + return args[idx + 1] + except ValueError: + pass + return None + + +def _is_flag_value(args: list[str], token: str) -> bool: + """True se o token é o valor de uma flag --key (ex: o 'x' em '--project x').""" + try: + idx = args.index(token) + except ValueError: + return False + return idx > 0 and args[idx - 1].startswith("--") + + +def _print_help() -> None: + print(f""" +{BOLD}squire alerts{RESET} — gerenciamento de alertas + + {CYAN}squire alerts{RESET} Lista alertas pendentes (alias de list) + {CYAN}squire alerts list{RESET} [--all] [--project ] Lista alertas (--all inclui reconhecidos) + {CYAN}squire alerts ack{RESET} [...] Reconhece alertas por índice + {CYAN}squire alerts ack{RESET} --all [--project ] [--task ] + Reconhece todos (com filtros opcionais) + {CYAN}squire alerts rm{RESET} [...] Remove alertas por índice + {CYAN}squire alerts rm{RESET} --acked | --all Remove reconhecidos / todos + +Índices referem-se à lista de não-reconhecidos. Com o dashboard ativo +(segundo escritor), prefira os seletores --project/--task. +""") + + +if __name__ == "__main__": + main() diff --git a/backends.py b/backends.py index d85aae9..8ff2eda 100644 --- a/backends.py +++ b/backends.py @@ -67,6 +67,32 @@ def _is_source_file(rel_path: str) -> bool: return False return p.suffix in _SOURCE_EXTENSIONS or p.name in _SOURCE_NAMES + +# Arquivos conhecidos sem extensão (válidos como último segmento de caminho) +_NO_EXT_FILES = {"dockerfile", "makefile", "procfile", "gemfile", "rakefile", + "vagrantfile", "jenkinsfile", "brewfile"} + +# Caminho relativo plausível: segmentos de [palavra . @ -], sem espaços, +# sem metacaracteres de shell (#, ", =, etc.) — rejeita o lixo que o Qwen +# derrama fora dos fences ('# src', 'rm -rf "', 'pytest==8.0.0', 'return a ') +_VALID_REL_PATH_RE = re.compile(r"^[A-Za-z0-9_.@-]+(/[A-Za-z0-9_.@-]+)*$") + + +def _is_plausible_relpath(path: str) -> bool: + """True se o candidato parece um caminho relativo legítimo de arquivo.""" + if not path or len(path) > 200: + return False + if path.startswith(("/", "-")): + return False + if not _VALID_REL_PATH_RE.match(path): + return False + parts = path.split("/") + if any(p in ("..", ".") for p in parts): + return False + last = parts[-1] + has_extension = "." in last.lstrip(".") or (last.startswith(".") and len(last) > 1) + return (has_extension and not last.endswith(".")) or last.lower() in _NO_EXT_FILES + # ── LLM global lock ──────────────────────────────────────────────── # Lock de arquivo para garantir que apenas um processo chama o llama.cpp # por vez. Evita saturação de CPU quando múltiplas ferramentas rodam juntas @@ -235,8 +261,24 @@ def _call_api_with_retry(self, instruction: str, timeout: int) -> dict: response.raise_for_status() return response.json() except httpx.HTTPStatusError as e: - if e.response.status_code < 500: - raise # 4xx: problema no request, não retry + code = e.response.status_code + if code < 500: + # 4xx: problema no request, não retry — mensagem acionável + if code in (401, 403): + raise RuntimeError( + f"LLM API recusou a chave (HTTP {code}) em {self.base_url} " + f"— verifique SQUIRE_LITELLM_KEY" + ) from e + if code == 404: + raise RuntimeError( + f"Modelo '{self.model}' não encontrado em {self.base_url} " + f"(HTTP 404) — verifique SQUIRE_LITELLM_MODEL e os modelos " + f"servidos pelo endpoint" + ) from e + raise RuntimeError( + f"LLM API retornou HTTP {code} em {self.base_url}: " + f"{e.response.text[:300]}" + ) from e last_exc = e except (httpx.ConnectError, httpx.RemoteProtocolError, httpx.ReadError, httpx.WriteError) as e: @@ -244,7 +286,15 @@ def _call_api_with_retry(self, instruction: str, timeout: int) -> dict: finally: client.close() - raise RuntimeError(f"LLM API falhou após {len(_HTTP_RETRY_DELAYS)} retries: {last_exc}") + if isinstance(last_exc, httpx.ConnectError): + raise RuntimeError( + f"Endpoint LLM inacessível em {self.base_url} — o serviço " + f"(Ollama/LiteLLM) está rodando? ({last_exc})" + ) + raise RuntimeError( + f"LLM API em {self.base_url} falhou após {len(_HTTP_RETRY_DELAYS)} " + f"retries: {last_exc}" + ) def _apply_changes(self, llm_response: str, project_path: Path) -> list[str]: """ @@ -261,6 +311,13 @@ def _apply_changes(self, llm_response: str, project_path: Path) -> list[str]: current_content: list[str] = [] pending_path: str | None = None + def _flush(path: str, content: list[str]) -> None: + try: + self._write_file(project_path, path, "\n".join(content)) + files_touched.append(path) + except ValueError as e: + print(f"⚠ fence ignorado ({e})") + for line in llm_response.split("\n"): stripped = line.strip() @@ -270,8 +327,7 @@ def _apply_changes(self, llm_response: str, project_path: Path) -> list[str]: if detected_path: if current_file: - self._write_file(project_path, current_file, "\n".join(current_content)) - files_touched.append(current_file) + _flush(current_file, current_content) current_file = detected_path current_content = [] elif current_file: @@ -281,8 +337,7 @@ def _apply_changes(self, llm_response: str, project_path: Path) -> list[str]: elif stripped == "```": if current_file: # fence de fechamento - self._write_file(project_path, current_file, "\n".join(current_content)) - files_touched.append(current_file) + _flush(current_file, current_content) current_file = None current_content = [] pending_path = None @@ -312,6 +367,10 @@ def _extract_filepath(self, fence_id: str) -> str | None: - ``src/foo.ts:`` → "src/foo.ts" (dois-pontos trailing — padrão Qwen) - ``Dockerfile`` → "Dockerfile" (sem extensão, sem barra) - ``.dockerignore`` → ".dockerignore" (começa com ponto) + + Candidatos implausíveis (espaços, metacaracteres, traversal, sem + extensão reconhecível) retornam None — o Qwen às vezes derrama texto + fora dos fences e linhas soltas viravam arquivos-lixo. """ if not fence_id: return None @@ -322,25 +381,29 @@ def _extract_filepath(self, fence_id: str) -> str | None: return None if ":" in candidate: - after_colon = candidate.split(":", 1)[1].strip() - if after_colon and ("/" in after_colon or "." in after_colon): - return after_colon.rstrip(": \t") - - # Caminho com barra ou extensão - if "/" in candidate or "." in candidate: - return candidate - - # Arquivos conhecidos sem extensão nem barra (Dockerfile, Makefile, etc.) - _NO_EXT_FILES = {"dockerfile", "makefile", "procfile", "gemfile", "rakefile", - "vagrantfile", "jenkinsfile", "brewfile"} - if candidate.lower() in _NO_EXT_FILES: + after_colon = candidate.split(":", 1)[1].strip().rstrip(": \t") + if after_colon and _is_plausible_relpath(after_colon): + return after_colon + # linguagem sem caminho (ex: "```typescript") ou caminho inválido + if ":" in candidate: + candidate = candidate.split(":", 1)[0].strip() + + if _is_plausible_relpath(candidate): return candidate return None def _write_file(self, project_path: Path, relative_path: str, content: str) -> None: - """Escreve arquivo no projeto, criando diretórios se necessário.""" - full_path = project_path / relative_path + """Escreve arquivo no projeto, criando diretórios se necessário. + + Defesa em profundidade: revalida o caminho e garante que o destino + resolvido fica dentro do projeto antes de escrever. + """ + if not _is_plausible_relpath(relative_path): + raise ValueError(f"caminho implausível: {relative_path!r}") + full_path = (project_path / relative_path).resolve() + if not full_path.is_relative_to(project_path.resolve()): + raise ValueError(f"caminho fora do projeto: {relative_path!r}") full_path.parent.mkdir(parents=True, exist_ok=True) full_path.write_text(content, encoding="utf-8") diff --git a/checkpoint.py b/checkpoint.py index 43d0fcf..77cc655 100644 --- a/checkpoint.py +++ b/checkpoint.py @@ -17,8 +17,9 @@ from pydantic import BaseModel from models import ( - Alert, AlertList, AlertSeverity, Checkpoint, GlobalStats, - History, HistoryEvent, Project, SessionLock, TaskList, + Alert, AlertList, AlertSeverity, Checkpoint, CommitLog, GlobalStats, + History, HistoryEvent, HomologationLog, HomologationLogEntry, Project, + SessionLock, TaskList, ) import config @@ -114,10 +115,51 @@ def append_event(project_id: str, event: HistoryEvent) -> None: save_history(project_id, history) +# ── Homologation log ─────────────────────────────────────────────── + +# Vereditos completos por task são limitados para o arquivo não crescer +# sem fim; 50 rodadas por task é muito acima de qualquer uso real. +HOMOLOG_LOG_CAP_PER_TASK = 50 + + +def load_homologation_log(project_id: str) -> HomologationLog: + result = load_model( + config.project_dir(project_id) / "homologation_log.json", HomologationLog + ) + return result or HomologationLog() + + +def save_homologation_log(project_id: str, log: HomologationLog) -> None: + save_model(config.project_dir(project_id) / "homologation_log.json", log) + + +def append_homologation_entry(project_id: str, entry: HomologationLogEntry) -> None: + """Adiciona um veredito ao log, mantendo as últimas N entradas por task.""" + log = load_homologation_log(project_id) + log.entries.append(entry) + same_task = [e for e in log.entries if e.task_id == entry.task_id] + if len(same_task) > HOMOLOG_LOG_CAP_PER_TASK: + excess = len(same_task) - HOMOLOG_LOG_CAP_PER_TASK + kept, dropped = [], 0 + for e in log.entries: + if e.task_id == entry.task_id and dropped < excess: + dropped += 1 + continue + kept.append(e) + log.entries = kept + save_homologation_log(project_id, log) + + # ── Session Lock ─────────────────────────────────────────────────── -def acquire_lock(session_id: str) -> bool: - """Tenta adquirir o lock. Retorna True se conseguiu.""" +def acquire_lock(session_id: str, project_id: Optional[str] = None) -> bool: + """Tenta adquirir o lock. Retorna True se conseguiu. + + `project_id` é gravado de forma estruturada no lock para que consumidores + (ex: dashboard) consigam fazer match exato de projeto sem precisar parsear + o `holder` por substring — substring casa prefixos diferentes (`proj` vs + `proj-happy`) e gera 409 falso-positivo. + """ existing = load_model(config.SESSION_LOCK_FILE, SessionLock) if existing is not None: @@ -134,6 +176,7 @@ def acquire_lock(session_id: str) -> bool: lock = SessionLock( holder=session_id, + project_id=project_id, acquired_at=datetime.now(timezone.utc), ttl_minutes=config.SESSION_LOCK_TTL_MINUTES, pid=os.getpid(), @@ -182,6 +225,18 @@ def add_alert( save_model(config.ALERTS_FILE, alerts) +# ── Commits ──────────────────────────────────────────────────────── + +def save_commits(project_id: str, commits: CommitLog) -> None: + """Persiste o log de commits do projeto em commits.json.""" + save_model(config.project_dir(project_id) / "commits.json", commits) + + +def load_commits(project_id: str) -> CommitLog: + result = load_model(config.project_dir(project_id) / "commits.json", CommitLog) + return result or CommitLog() + + # ── Global Stats ─────────────────────────────────────────────────── def load_stats() -> GlobalStats: diff --git a/config.py b/config.py index 4a2c65b..9ebe6a5 100644 --- a/config.py +++ b/config.py @@ -10,8 +10,8 @@ # ── Paths ────────────────────────────────────────────────────────── -# Raiz do estado persistente (volume Unraid montado na VM) -STATE_ROOT = Path(os.environ["SQUIRE_STATE_ROOT"]) +# Raiz do estado persistente (disco local da VM; mesmo default do wrapper bash) +STATE_ROOT = Path(os.getenv("SQUIRE_STATE_ROOT", "/home/ai-debian/squire-state")) PROJECTS_DIR = STATE_ROOT / "projects" ALERTS_FILE = STATE_ROOT / "alerts.json" @@ -78,6 +78,10 @@ def _load_budget_file() -> dict: # Rodadas máximas por task (cada rodada = inner loop até 10 + 1 homologação) MAX_HOMOLOGATION_ATTEMPTS = int(os.getenv("SQUIRE_MAX_HOMOLOG", "5")) +# Timeout (s) do implement_directly — tasks de docs/multiarquivo podem +# passar fácil dos 5 min de geração +IMPLEMENT_TIMEOUT_SECONDS = int(os.getenv("SQUIRE_IMPLEMENT_TIMEOUT", "600")) + # Quantas rejeições consecutivas com o mesmo padrão de erro disparam escalação imediata LOOP_DETECT_THRESHOLD = int(os.getenv("SQUIRE_LOOP_DETECT", "3")) @@ -148,6 +152,23 @@ def compute_cost_usd(prompt_tokens: int, completion_tokens: int, model: str) -> HEARTBEAT_INTERVAL_SECONDS = int(os.getenv("SQUIRE_HEARTBEAT", "300")) # 5 min +# ── Command queue (dashboard → agente host) ──────────────────────── + +COMMANDS_DIR = STATE_ROOT / "commands" +COMMANDS_PENDING = COMMANDS_DIR / "pending" +COMMANDS_RUNNING = COMMANDS_DIR / "running" +COMMANDS_DONE = COMMANDS_DIR / "done" + +# Resultados em done/ mais velhos que isto são apagados pelo agente +COMMAND_RESULT_TTL_HOURS = int(os.getenv("SQUIRE_COMMAND_TTL_H", "24")) +# Timeout de execução de um comando (plan_tasks pode demorar minutos) +COMMAND_TIMEOUT_SECONDS = int(os.getenv("SQUIRE_COMMAND_TIMEOUT", "900")) +# Intervalo de polling do agente +AGENT_POLL_SECONDS = float(os.getenv("SQUIRE_AGENT_POLL", "2")) +# Raiz permitida para repo_path de projetos criados via fila +AGENT_REPO_ROOT = Path(os.getenv("SQUIRE_AGENT_REPO_ROOT", "/home/ai-debian/projects")) + + # ── Helpers ──────────────────────────────────────────────────────── def project_dir(project_id: str) -> Path: diff --git a/dashboard/.dockerignore b/dashboard/.dockerignore new file mode 100644 index 0000000..09a8d13 --- /dev/null +++ b/dashboard/.dockerignore @@ -0,0 +1,39 @@ +# Dependencies +node_modules +npm-debug.log + +# Next.js +.next +out + +# Git +.git +.gitignore + +# IDE +.idea +.vscode +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Docker +Dockerfile +docker-compose.yml +.dockerignore + +# Documentation +README.md +LICENSE + +# Testing +coverage +.nyc_output + +# Environment +.env +.env.local +.env.production.local diff --git a/dashboard/.gitignore b/dashboard/.gitignore new file mode 100644 index 0000000..6a1199f --- /dev/null +++ b/dashboard/.gitignore @@ -0,0 +1,38 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts +.aider* +e2e/.state diff --git a/dashboard/CLAUDE.md b/dashboard/CLAUDE.md new file mode 100644 index 0000000..6ef72b2 --- /dev/null +++ b/dashboard/CLAUDE.md @@ -0,0 +1,84 @@ +# CLAUDE.md — Squire Dashboard + +Dashboard Next.js 14 (App Router) que visualiza e **edita** o estado do squire. +Lê arquivos JSON do filesystem; escreve alertas (ack/dismiss), tasks (CRUD), +project.json e budget direto nos arquivos (lock-checked), e enfileira +operações de host (criar projeto, run/resume/kill, planejar com Claude) em +`/data/commands/pending/` para o **agente host** (`squire agent` rodando na +VM — sem ele, essas operações ficam pendentes para sempre). + +Escrita exige `Authorization: Bearer $DASHBOARD_WRITE_TOKEN` (env no +compose, valor no `.env` gitignored ao lado; sem env → 503). O usuário faz +login em `/login`; o token vai para localStorage e `authedFetch` injeta. + +Deploy como container Docker **na VM Ai-Debian** via `docker compose up -d` +(porta 3101, volume rw de /home/ai-debian/squire-state, user 1000:1000) — +não no Unraid, que não enxerga o disco local da VM. + +## Stack +- Next.js 14 App Router + TypeScript 5 + Tailwind CSS 3 +- Vitest + @testing-library/react para testes +- Server Components por padrão (sem `use client` a menos que necessário) + +## Contrato de dados — squire Python models + +Os JSONs escritos pelo squire devem ser lidos com estes tipos TypeScript (ver src/lib/types.ts): + +**Enums (string literals):** +- ProjectStatus: 'planning' | 'implementing' | 'reviewing' | 'blocked' | 'completed' +- TaskStatus: 'pending' | 'implementing' | 'testing' | 'homologating' | 'completed' | 'blocked' +- CursorStep: 'planning' | 'red_phase' | 'llm_execution' | 'testing' | 'homologation' | 'completed' +- Effort: 'low' | 'medium' | 'high' +- TestAuthor: 'claude' | 'local' +- EventType: 'task_started' | 'implementation_cycle' | 'tests_passed' | 'tests_failed' | 'homologation_requested' | 'homologation_approved' | 'homologation_failed' | 'escalation_created' | 'task_completed' | 'session_started' | 'session_resumed' | 'session_ended' +- AlertSeverity: 'warning' | 'critical' +- Actor: 'local_llm' | 'claude_code' | 'squire' | 'human' + +**JSON file structure:** +``` +{DATA_PATH}/global-stats.json → GlobalStats (snake_case fields) +{DATA_PATH}/alerts.json → { "alerts": Alert[] } ← AlertList WRAPPER +{DATA_PATH}/projects/{id}/project.json → Project +{DATA_PATH}/projects/{id}/tasks.json → { "tasks": Task[] } ← TaskList WRAPPER +{DATA_PATH}/projects/{id}/history.json → { "events": HistoryEvent[] } ← History WRAPPER +{DATA_PATH}/projects/{id}/checkpoint.json → Checkpoint +{DATA_PATH}/projects/{id}/commits.json → { "commits": CommitSummary[] } ← CommitLog WRAPPER +``` + +**KEY GlobalStats fields (snake_case, not camelCase):** +- daily_claude_code_calls, daily_local_llm_calls, cost_estimate_usd +- tasks_completed_today, approval_first_try_rate, date, projects_touched_today + +**Dev data path:** `process.env.SQUIRE_DATA_PATH ?? path.join(cwd, 'fixtures', 'data')` + +## Componentes implementados + +- `ProjectCard` — card de projeto com barra de progresso +- `AlertBanner` — banner fixo no topo com alertas critical/warning +- `GlobalStats` — 5 cards de métricas (server component, aceita props) +- `TaskList` — lista expansível de tasks com badges de effort/TDD/rejeição +- `Timeline` — linha do tempo de HistoryEvent com cores por Actor +- `CommitLog` — histórico de commits com diff_summary +- `RefreshController` — trigger de auto-refresh (client component) +- `RateLimitGauge` — gauge de consumo do budget Claude Code +- `CheckpointPanel` — inspetor de estado da sessão (cursor + context + recovery) +- `TDDProgressBar` — visualizador das fases RED/GREEN/REFACTOR + +## Checklist de homologação + +Ao revisar código, verificar: +1. Todos os campos de JSON usam snake_case (não camelCase) — igual ao Python +2. AlertList/TaskList/History/CommitLog são lidos com seus wrappers (.alerts / .tasks / .events / .commits) +3. GlobalStats usa daily_claude_code_calls (não llm_calls.claude_code) +4. AlertSeverity aceita apenas 'warning' e 'critical' (não 'error' nem 'info') +5. Actor usa 'local_llm', 'claude_code', 'squire', 'human' (não 'system', 'agent', etc.) +6. Server components não usam hooks (useEffect, useState) nem 'use client' +7. Testes existem e passam (npm test) +8. Nenhum `any` no TypeScript +9. Fixtures em fixtures/data/ têm estrutura correta com wrappers + +## Convenções +- Commits: conventional commits em inglês (feat: / fix: / chore:) +- Idioma da UI: português brasileiro +- Sem CSS Modules — apenas Tailwind +- Imports de tipos de '@/lib/types', funções de '@/lib/data' diff --git a/dashboard/Dockerfile b/dashboard/Dockerfile new file mode 100644 index 0000000..92cbb9f --- /dev/null +++ b/dashboard/Dockerfile @@ -0,0 +1,46 @@ +# Stage 1: Dependencies +FROM node:20-alpine AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Copy package files +COPY package.json package-lock.json* ./ +RUN npm ci + +# Stage 2: Builder +FROM node:20-alpine AS builder +WORKDIR /app + +# Copy dependencies from deps stage +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Enable standalone output in Next.js +ENV NEXT_TELEMETRY_DISABLED 1 + +# Build the application +RUN npm run build + +# Stage 3: Runner +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV production +ENV NEXT_TELEMETRY_DISABLED 1 + +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +# Copia apenas o output standalone — public, static e server.js +COPY --from=builder --chown=nextjs:nodejs /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 +ENV HOSTNAME "0.0.0.0" + +CMD ["node", "server.js"] diff --git a/dashboard/README.md b/dashboard/README.md new file mode 100644 index 0000000..452977b --- /dev/null +++ b/dashboard/README.md @@ -0,0 +1,48 @@ +# Squire Dashboard + +Visualização em tempo real do estado do squire (orquestrador local + Claude Code). Next.js 14, TypeScript, Tailwind CSS. + +## Estrutura do Projeto + +- `src/app/`: App Router do Next.js (rotas, layouts, páginas) +- `src/components/`: Componentes React reutilizáveis +- `src/lib/`: Funções utilitárias e lógica de negócios +- `src/hooks/`: Custom React Hooks +- `public/`: Arquivos estáticos + +## Configuração de Path Aliases + +O projeto está configurado para usar aliases baseados em `@`: +- `@/components` → `src/components` +- `@/lib` → `src/lib` +- `@/hooks` → `src/hooks` +- `@/app` → `src/app` + +## Comandos Disponíveis + +```bash +# Instalar dependências +npm install + +# Iniciar desenvolvimento +npm run dev + +# Build para produção +npm run build + +# Iniciar servidor de produção +npm start + +# Lint +npm run lint +## Testes + +```bash +npm test # unit/integração (vitest, 165+ testes) +npm run test:e2e # Playwright contra um estado seedado (e2e/.state) +``` + +A suite E2E sobe `next dev` na porta 3199 com `SQUIRE_DATA_PATH` apontando +para a fixture recriada pelo `e2e/global-setup.ts` e token `e2e-token`. +O spec `agent-roundtrip` executa o `squire agent --once` real (pulado se o +repo do squire não estiver em `/home/ai-debian/squire`). diff --git a/dashboard/docker-compose.yml b/dashboard/docker-compose.yml new file mode 100644 index 0000000..fa4b27c --- /dev/null +++ b/dashboard/docker-compose.yml @@ -0,0 +1,42 @@ +# Standalone (apenas dashboard) — útil para dev isolado. +# Deploy canônico é a stack em ../deploy/docker-compose.yml (workspace + +# dashboard juntos, volume nomeado squire-state). Este arquivo bind-monta o +# path legado /home/ai-debian/squire-state e é superseded pela stack. +version: '3.8' + +services: + squire-dashboard: + build: + context: . + dockerfile: Dockerfile + container_name: squire-dashboard + restart: unless-stopped + # uid 1000 (ai-debian): os arquivos de estado são 0600 ai-debian e o + # dashboard precisa de escrita (ack/dismiss de alertas via POST API) + user: "1000:1000" + ports: + # 3100 está ocupada na VM (browserless) + - "3101:3000" + environment: + - NODE_ENV=production + - PORT=3000 + - HOSTNAME=0.0.0.0 + # Aponta para o volume montado com os JSONs do squire + - SQUIRE_DATA_PATH=/data + # Intervalo de polling em ms (default: 30000) + - NEXT_PUBLIC_REFRESH_INTERVAL=30000 + # Token de escrita (gerado no .env ao lado deste arquivo — gitignored). + # Sem ele, todas as rotas de escrita respondem 503. + - DASHBOARD_WRITE_TOKEN=${DASHBOARD_WRITE_TOKEN} + volumes: + # Estado do squire em rw: o dashboard é segundo escritor (POST + # /api/alerts/ack escreve acknowledged/dismiss em alerts.json) + - /home/ai-debian/squire-state:/data + healthcheck: + # 127.0.0.1 (não localhost): o busybox wget resolve localhost para ::1 + # e o node escuta só em IPv4 → connection refused + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/api/health || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 15s diff --git a/dashboard/e2e/agent-roundtrip.spec.ts b/dashboard/e2e/agent-roundtrip.spec.ts new file mode 100644 index 0000000..6cab2ae --- /dev/null +++ b/dashboard/e2e/agent-roundtrip.spec.ts @@ -0,0 +1,58 @@ +import { test, expect } from '@playwright/test'; +import { execFileSync } from 'child_process'; +import { existsSync, mkdirSync, readdirSync, rmSync } from 'fs'; +import { join } from 'path'; +import { loginByToken } from './helpers'; +import { E2E_STATE_DIR } from '../playwright.config'; + +/** + * Roundtrip completo com o agente real (local-only): enfileira new_project + * pela UI, roda `squire agent --once` contra o estado E2E e confere o + * projeto aparecendo. Pulado quando o repo do squire não está na máquina. + */ + +const SQUIRE_DIR = '/home/ai-debian/squire'; +const REPO_ROOT = join(E2E_STATE_DIR, 'repos'); + +test.describe('roundtrip com o agente host', () => { + test.skip(!existsSync(join(SQUIRE_DIR, 'agent_cli.py')), 'repo do squire ausente'); + + test('new_project pela UI → agent --once → projeto criado', async ({ page }) => { + await loginByToken(page); + mkdirSync(REPO_ROOT, { recursive: true }); + + await page.goto('/projects/new'); + await page.getByLabel('ID do projeto *').fill('e2e-roundtrip'); + await page + .getByRole('textbox', { name: 'Repositório' }) + .fill(join(REPO_ROOT, 'e2e-roundtrip')); + await page.getByRole('button', { name: 'Criar projeto' }).click(); + await expect(page.getByText(/Aguardando o agente|Criando projeto/)).toBeVisible(); + + // Espera o comando chegar na fila, então roda o agente uma vez + const pendingDir = join(E2E_STATE_DIR, 'commands', 'pending'); + await expect + .poll(() => readdirSync(pendingDir).length, { timeout: 5000 }) + .toBeGreaterThan(0); + + execFileSync(join(SQUIRE_DIR, 'squire'), ['agent', '--once'], { + env: { + ...process.env, + SQUIRE_STATE_ROOT: E2E_STATE_DIR, + SQUIRE_AGENT_REPO_ROOT: REPO_ROOT, + }, + timeout: 60_000, + }); + + // O poll do form detecta done e navega para a página do projeto + await page.waitForURL('/projects/e2e-roundtrip', { timeout: 15_000 }); + expect( + existsSync(join(E2E_STATE_DIR, 'projects', 'e2e-roundtrip', 'tasks.json')) + ).toBe(true); + + rmSync(join(E2E_STATE_DIR, 'projects', 'e2e-roundtrip'), { + recursive: true, + force: true, + }); + }); +}); diff --git a/dashboard/e2e/alerts.spec.ts b/dashboard/e2e/alerts.spec.ts new file mode 100644 index 0000000..3061304 --- /dev/null +++ b/dashboard/e2e/alerts.spec.ts @@ -0,0 +1,38 @@ +import { test, expect } from '@playwright/test'; +import { readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import { loginByToken } from './helpers'; +import { E2E_STATE_DIR } from '../playwright.config'; + +const ALERTS_PATH = join(E2E_STATE_DIR, 'alerts.json'); + +test.describe('alertas', () => { + let original: string; + + test.beforeEach(() => { + original = readFileSync(ALERTS_PATH, 'utf8'); + }); + + test.afterEach(() => { + // Restaura o estado — outros specs dependem do alerta não-reconhecido + writeFileSync(ALERTS_PATH, original); + }); + + test('ack pelo banner persiste acknowledged=true', async ({ page }) => { + await loginByToken(page); + await page.goto('/'); + + const banner = page.getByText('Task travou após 5 homologações'); + await expect(banner).toBeVisible(); + await page.getByRole('button', { name: 'Reconhecer alerta' }).first().click(); + + await expect + .poll(() => { + const alerts = JSON.parse( + readFileSync(join(E2E_STATE_DIR, 'alerts.json'), 'utf8') + ).alerts; + return alerts[0]?.acknowledged; + }, { timeout: 5000 }) + .toBe(true); + }); +}); diff --git a/dashboard/e2e/blocked-triage.spec.ts b/dashboard/e2e/blocked-triage.spec.ts new file mode 100644 index 0000000..6ca5655 --- /dev/null +++ b/dashboard/e2e/blocked-triage.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@playwright/test'; +import { readdirSync, readFileSync } from 'fs'; +import { join } from 'path'; +import { loginByToken } from './helpers'; +import { E2E_STATE_DIR } from '../playwright.config'; + +test.describe('triagem de task bloqueada', () => { + test.beforeEach(async ({ page }) => { + await loginByToken(page); + }); + + test('painel mostra o veredito completo do log', async ({ page }) => { + await page.goto('/projects/proj-blocked'); + await page.getByText('Camada de persistência').click(); // expande a task + + const panel = page.getByTestId('blocked-task-panel'); + await expect(panel).toBeVisible(); + await expect(panel.getByText('Rodada 5')).toBeVisible(); + await expect( + panel.getByText('save() retorna Week mas a rota espera tupla') + ).toBeVisible(); + + // Expande o veredito → fix_suggestion + feedback completos + await panel.getByText('save() retorna Week mas a rota espera tupla').click(); + await expect(panel.getByText('Retorne (week, week_id) em save().')).toBeVisible(); + await expect( + panel.getByText('Incompatibilidade crítica entre repository.save()') + ).toBeVisible(); + }); + + test('Corrigir com Claude enfileira fix_task em commands/pending', async ({ page }) => { + await page.goto('/projects/proj-blocked'); + await page.getByText('Camada de persistência').click(); + + page.once('dialog', (d) => d.accept()); + await page + .getByTestId('blocked-task-panel') + .getByRole('button', { name: /corrigir com claude/i }) + .click(); + await expect(page.getByText('Aguardando o agente…')).toBeVisible(); + + // O agente não roda no E2E — o comando precisa estar na fila + const pendingDir = join(E2E_STATE_DIR, 'commands', 'pending'); + await expect + .poll(() => readdirSync(pendingDir).length, { timeout: 5000 }) + .toBeGreaterThan(0); + const file = readdirSync(pendingDir)[0]; + const cmd = JSON.parse(readFileSync(join(pendingDir, file), 'utf8')); + expect(cmd).toMatchObject({ + type: 'fix_task', + project_id: 'proj-blocked', + args: { task_id: 'task-009' }, + requested_by: 'dashboard', + }); + }); +}); diff --git a/dashboard/e2e/chrome.spec.ts b/dashboard/e2e/chrome.spec.ts new file mode 100644 index 0000000..f3ef4a5 --- /dev/null +++ b/dashboard/e2e/chrome.spec.ts @@ -0,0 +1,56 @@ +import { test, expect } from '@playwright/test'; +import { unlinkSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import { E2E_STATE_DIR } from '../playwright.config'; + +/** + * Regressão da classe "chrome congelado": Sidebar e HealthStrip leem o + * estado a cada request em TODAS as rotas — inclusive as que eram + * estaticamente prerenderizadas (/login, /projects/new), onde a sidebar + * aparecia vazia e a sessão era mentirosa. Se alguém reintroduzir + * prerender no layout, estes specs quebram. + */ + +const LOCK_PATH = join(E2E_STATE_DIR, 'session.lock'); +const CHROME_ROUTES = ['/projects/new', '/login']; + +test.describe('chrome do layout é sempre fresco', () => { + for (const route of CHROME_ROUTES) { + test(`sidebar lista projetos em ${route}`, async ({ page }) => { + await page.goto(route); + await expect( + page.getByRole('link', { name: /Projeto Bloqueado/ }) + ).toBeVisible(); + await expect( + page.getByRole('link', { name: /Projeto Ativo/ }) + ).toBeVisible(); + }); + } + + test('HealthStrip ocioso: copy correta (sem "Sem sessão sem lock")', async ({ page }) => { + await page.goto('/projects/new'); + await expect(page.getByText('Squire ocioso')).toBeVisible(); + await expect(page.getByText('nenhuma sessão ativa')).toBeVisible(); + await expect(page.getByText('Sem sessão')).not.toBeVisible(); + }); + + test('HealthStrip reflete lock ativo mesmo em rota antes-estática', async ({ page }) => { + writeFileSync( + LOCK_PATH, + JSON.stringify({ + holder: 'sess-e2e-chrome', + project_id: 'proj-active', + acquired_at: new Date().toISOString(), + ttl_minutes: 60, + pid: process.pid, + }) + ); + try { + await page.goto('/login'); + await expect(page.getByText('Squire ativo')).toBeVisible(); + await expect(page.getByText('proj-active', { exact: true })).toBeVisible(); + } finally { + unlinkSync(LOCK_PATH); + } + }); +}); diff --git a/dashboard/e2e/cli-hints.spec.ts b/dashboard/e2e/cli-hints.spec.ts new file mode 100644 index 0000000..d701251 --- /dev/null +++ b/dashboard/e2e/cli-hints.spec.ts @@ -0,0 +1,60 @@ +import { test, expect } from '@playwright/test'; +import { unlinkSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import { E2E_STATE_DIR } from '../playwright.config'; + +const LOCK_PATH = join(E2E_STATE_DIR, 'session.lock'); + +test.describe('comandos de desobstrução', () => { + test('task bloqueada mostra fix/unblock/reset e copia ao clicar', async ({ + page, + context, + }) => { + await context.grantPermissions(['clipboard-read', 'clipboard-write']); + await page.goto('/projects/proj-blocked'); + await page.getByText('Camada de persistência').click(); + + const panel = page.getByTestId('blocked-task-panel'); + await expect(panel.getByText('squire fix proj-blocked task-009')).toBeVisible(); + await expect( + panel.getByText('squire unblock proj-blocked task-009') + ).toBeVisible(); + await expect(panel.getByText('squire reset proj-blocked task-009')).toBeVisible(); + + await panel.getByText('squire fix proj-blocked task-009').click(); + await expect(panel.getByText('copiado ✓')).toBeVisible(); + const copied = await page.evaluate(() => navigator.clipboard.readText()); + expect(copied).toBe('squire fix proj-blocked task-009'); + }); + + test('projeto parado com pendentes mostra run/bg; bloqueado mostra unblock', async ({ + page, + }) => { + await page.goto('/projects/proj-active'); + await expect(page.getByText('squire run proj-active')).toBeVisible(); + await expect(page.getByText('squire bg proj-active')).toBeVisible(); + + await page.goto('/projects/proj-blocked'); + await expect(page.getByText('squire unblock proj-blocked', { exact: true })).toBeVisible(); + }); + + test('com sessão ativa DESTE projeto mostra squire kill', async ({ page }) => { + writeFileSync( + LOCK_PATH, + JSON.stringify({ + holder: 'sess-e2e', + project_id: 'proj-active', + acquired_at: new Date().toISOString(), + ttl_minutes: 60, + pid: process.pid, + }) + ); + try { + await page.goto('/projects/proj-active'); + await expect(page.getByText('squire kill')).toBeVisible(); + await expect(page.getByText('squire run proj-active')).not.toBeVisible(); + } finally { + unlinkSync(LOCK_PATH); + } + }); +}); diff --git a/dashboard/e2e/global-setup.ts b/dashboard/e2e/global-setup.ts new file mode 100644 index 0000000..8408dab --- /dev/null +++ b/dashboard/e2e/global-setup.ts @@ -0,0 +1,161 @@ +import { mkdirSync, rmSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import { E2E_STATE_DIR } from '../playwright.config'; + +/** + * Seed do estado para os testes E2E: + * - proj-blocked: 1 task bloqueada com homologation_log completo + * - proj-active: implementing, 2 tasks (1 done, 1 pending) + * - 1 alerta crítico, global-stats, fila commands/ vazia + */ + +const NOW = '2026-06-11T12:00:00+00:00'; + +function task(over: Record) { + return { + id: 'task-001', + title: 'Task', + description: '', + status: 'pending', + assigned_to: 'local_llm', + attempts: 0, + max_attempts: 10, + homologation_result: null, + homologation_attempt: 0, + max_homologation_attempts: 5, + completed_at: null, + claude_code_assisted: false, + subtasks: [], + rejection_summaries: [], + no_progress_streak: 0, + skip_homologation: false, + effort: 'medium', + tdd: true, + test_author: 'claude', + max_usd: null, + cost_usd: 0, + ...over, + }; +} + +function project(id: string, over: Record) { + return { + id, + name: id, + description: '', + repo_path: `/tmp/e2e-repos/${id}`, + stack: ['python'], + status: 'implementing', + created_at: NOW, + updated_at: NOW, + current_task_id: null, + coding_backend: 'opencode', + ...over, + }; +} + +function write(path: string, data: unknown) { + writeFileSync(path, JSON.stringify(data, null, 2)); +} + +export default function globalSetup() { + rmSync(E2E_STATE_DIR, { recursive: true, force: true }); + for (const dir of [ + 'projects/proj-blocked', + 'projects/proj-active', + 'commands/pending', + 'commands/running', + 'commands/done', + ]) { + mkdirSync(join(E2E_STATE_DIR, dir), { recursive: true }); + } + + // proj-blocked: task bloqueada com vereditos completos + write( + join(E2E_STATE_DIR, 'projects/proj-blocked/project.json'), + project('proj-blocked', { name: 'Projeto Bloqueado', status: 'blocked' }) + ); + write(join(E2E_STATE_DIR, 'projects/proj-blocked/tasks.json'), { + tasks: [ + task({ + id: 'task-009', + title: 'Camada de persistência', + status: 'blocked', + homologation_attempt: 5, + homologation_result: 'rejected', + rejection_summaries: ['resumo antigo sem log'], + }), + ], + }); + write(join(E2E_STATE_DIR, 'projects/proj-blocked/homologation_log.json'), { + entries: [ + { + timestamp: NOW, + task_id: 'task-009', + attempt: 5, + approved: false, + summary: 'save() retorna Week mas a rota espera tupla', + feedback: 'Incompatibilidade crítica entre repository.save() e routes.py.', + fix_suggestion: 'Retorne (week, week_id) em save().', + suggestions: [], + source: 'session', + cost_usd: 0.04, + model: 'claude-x', + }, + ], + }); + write(join(E2E_STATE_DIR, 'projects/proj-blocked/history.json'), { + events: [ + { + timestamp: NOW, + type: 'homologation_failed', + task_id: 'task-009', + attempt: 5, + summary: 'save() retorna Week mas a rota espera tupla', + actor: 'claude_code', + }, + ], + }); + + // proj-active: implementing + write( + join(E2E_STATE_DIR, 'projects/proj-active/project.json'), + project('proj-active', { name: 'Projeto Ativo' }) + ); + write(join(E2E_STATE_DIR, 'projects/proj-active/tasks.json'), { + tasks: [ + task({ id: 'task-001', title: 'Setup', status: 'completed' }), + task({ id: 'task-002', title: 'Feature pendente' }), + ], + }); + write(join(E2E_STATE_DIR, 'projects/proj-active/history.json'), { events: [] }); + + // globais + write(join(E2E_STATE_DIR, 'alerts.json'), { + alerts: [ + { + project_id: 'proj-blocked', + severity: 'critical', + type: 'max_homologations_reached', + task_id: 'task-009', + message: 'Task travou após 5 homologações', + created_at: NOW, + acknowledged: false, + }, + ], + }); + write(join(E2E_STATE_DIR, 'global-stats.json'), { + daily_claude_code_calls: 3, + daily_local_llm_calls: 12, + date: '2026-06-11', + cost_estimate_usd: 1.5, + daily_tokens: 1000, + cost_by_model: {}, + daily_calls_unknown_cost: 0, + projects_touched_today: ['proj-active'], + tasks_completed_today: 1, + tasks_homologated_today: 1, + tasks_approved_first_try_today: 1, + approval_first_try_rate: 100, + }); +} diff --git a/dashboard/e2e/helpers.ts b/dashboard/e2e/helpers.ts new file mode 100644 index 0000000..24fa117 --- /dev/null +++ b/dashboard/e2e/helpers.ts @@ -0,0 +1,9 @@ +import { Page } from '@playwright/test'; +import { E2E_TOKEN } from '../playwright.config'; + +/** Injeta o token de escrita no localStorage (equivalente a logar em /login). */ +export async function loginByToken(page: Page) { + await page.addInitScript((token) => { + window.localStorage.setItem('squire_write_token', token); + }, E2E_TOKEN); +} diff --git a/dashboard/e2e/home.spec.ts b/dashboard/e2e/home.spec.ts new file mode 100644 index 0000000..b53d3e8 --- /dev/null +++ b/dashboard/e2e/home.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; + +test.describe('home', () => { + test('lista projetos seedados com status e contagem de bloqueios', async ({ page }) => { + await page.goto('/'); + // headings dos cards (o nome também aparece na sidebar — escopo via role) + await expect( + page.getByRole('heading', { name: 'Projeto Bloqueado' }) + ).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Projeto Ativo' })).toBeVisible(); + // aparece no card E na pill da sidebar (agora também em PT) + await expect(page.getByText('Bloqueado', { exact: true }).first()).toBeVisible(); + await expect(page.getByText('1 bloqueada')).toBeVisible(); + }); + + test('mostra o alerta crítico seedado', async ({ page }) => { + await page.goto('/'); + await expect(page.getByText('Task travou após 5 homologações')).toBeVisible(); + }); +}); diff --git a/dashboard/e2e/login.spec.ts b/dashboard/e2e/login.spec.ts new file mode 100644 index 0000000..e91aa7a --- /dev/null +++ b/dashboard/e2e/login.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from '@playwright/test'; +import { E2E_TOKEN } from '../playwright.config'; + +test.describe('login', () => { + test('token errado mostra erro', async ({ page }) => { + await page.goto('/login'); + await page.getByPlaceholder('Token').fill('token-errado'); + await page.getByRole('button', { name: 'Entrar' }).click(); + await expect(page.getByText('Token inválido.')).toBeVisible(); + }); + + test('token certo redireciona e persiste', async ({ page }) => { + await page.goto('/login'); + await page.getByPlaceholder('Token').fill(E2E_TOKEN); + await page.getByRole('button', { name: 'Entrar' }).click(); + await page.waitForURL('/'); + const stored = await page.evaluate(() => + window.localStorage.getItem('squire_write_token') + ); + expect(stored).toBe(E2E_TOKEN); + }); +}); diff --git a/dashboard/e2e/run-control.spec.ts b/dashboard/e2e/run-control.spec.ts new file mode 100644 index 0000000..b90e939 --- /dev/null +++ b/dashboard/e2e/run-control.spec.ts @@ -0,0 +1,46 @@ +import { test, expect } from '@playwright/test'; +import { unlinkSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import { loginByToken } from './helpers'; +import { E2E_STATE_DIR, E2E_TOKEN } from '../playwright.config'; + +const LOCK_PATH = join(E2E_STATE_DIR, 'session.lock'); + +test.describe('run control com sessão ativa', () => { + test.beforeEach(async ({ page }) => { + await loginByToken(page); + writeFileSync( + LOCK_PATH, + JSON.stringify({ + holder: 'sess-e2e', + project_id: 'proj-active', + acquired_at: new Date().toISOString(), + ttl_minutes: 60, + pid: process.pid, + }) + ); + }); + + test.afterEach(() => { + try { + unlinkSync(LOCK_PATH); + } catch { + // já removido + } + }); + + test('Run/Resume desabilitados; Kill habilitado no projeto do lock', async ({ page }) => { + await page.goto('/projects/proj-active'); + await expect(page.getByRole('button', { name: 'Run', exact: true })).toBeDisabled(); + await expect(page.getByRole('button', { name: 'Resume', exact: true })).toBeDisabled(); + await expect(page.getByRole('button', { name: 'Kill', exact: true })).toBeEnabled(); + }); + + test('POST run devolve 409 com lock ativo', async ({ request }) => { + const res = await request.post('/api/commands', { + headers: { Authorization: `Bearer ${E2E_TOKEN}` }, + data: { type: 'run', project_id: 'proj-active' }, + }); + expect(res.status()).toBe(409); + }); +}); diff --git a/dashboard/e2e/tasks-crud.spec.ts b/dashboard/e2e/tasks-crud.spec.ts new file mode 100644 index 0000000..f9bcc5f --- /dev/null +++ b/dashboard/e2e/tasks-crud.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test'; +import { loginByToken } from './helpers'; + +test.describe('task CRUD pelo modal', () => { + test.beforeEach(async ({ page }) => { + await loginByToken(page); + }); + + test('cria, edita e exclui uma task', async ({ page }) => { + await page.goto('/projects/proj-active'); + + // Criar + await page.getByRole('button', { name: '+ Nova task' }).click(); + await page.getByLabel('Título *').fill('Task criada no E2E'); + await page.getByRole('button', { name: 'Criar task' }).click(); + await expect( + page.getByRole('heading', { name: 'Task criada no E2E' }) + ).toBeVisible(); + + // Helper: linha (header) da task pelo heading — sobe ao container do header + const rowOf = (title: string) => + page + .getByRole('heading', { name: title }) + .locator('xpath=ancestor::div[contains(@class,"p-4")][1]'); + + // Editar (menu ⋯ da task recém-criada) + await rowOf('Task criada no E2E').getByTitle('Ações da task').click(); + await page.getByRole('menuitem', { name: 'Editar task' }).click(); + const titleInput = page.getByLabel('Título *'); + await titleInput.fill('Task editada no E2E'); + await page.getByRole('button', { name: 'Salvar' }).click(); + await expect( + page.getByRole('heading', { name: 'Task editada no E2E' }) + ).toBeVisible(); + + // Excluir + page.once('dialog', (d) => d.accept()); + await rowOf('Task editada no E2E').getByTitle('Ações da task').click(); + await page.getByRole('menuitem', { name: 'Excluir task' }).click(); + await expect( + page.getByRole('heading', { name: 'Task editada no E2E' }) + ).not.toBeVisible(); + }); +}); diff --git a/dashboard/fixtures/data/alerts.json b/dashboard/fixtures/data/alerts.json new file mode 100644 index 0000000..d17de10 --- /dev/null +++ b/dashboard/fixtures/data/alerts.json @@ -0,0 +1,13 @@ +{ + "alerts": [ + { + "project_id": "squire-dashboard", + "severity": "warning", + "type": "no_progress", + "task_id": "task-005", + "message": "Sem progresso após 3 ciclos consecutivos", + "created_at": "2026-03-29T10:00:00Z", + "acknowledged": false + } + ] +} diff --git a/dashboard/fixtures/data/global-stats.json b/dashboard/fixtures/data/global-stats.json new file mode 100644 index 0000000..3d5eaba --- /dev/null +++ b/dashboard/fixtures/data/global-stats.json @@ -0,0 +1,9 @@ +{ + "daily_claude_code_calls": 12, + "daily_local_llm_calls": 47, + "date": "2026-03-29", + "cost_estimate_usd": 0.42, + "projects_touched_today": ["squire-dashboard"], + "tasks_completed_today": 3, + "approval_first_try_rate": 75.0 +} diff --git a/dashboard/fixtures/data/projects/minimal-template/homologation_log.json b/dashboard/fixtures/data/projects/minimal-template/homologation_log.json new file mode 100644 index 0000000..0de6303 --- /dev/null +++ b/dashboard/fixtures/data/projects/minimal-template/homologation_log.json @@ -0,0 +1,17 @@ +{ + "entries": [ + { + "timestamp": "2026-06-11T12:00:00Z", + "task_id": "task-001", + "attempt": 1, + "approved": false, + "summary": "resumo", + "feedback": "feedback completo", + "fix_suggestion": "conserte X", + "suggestions": [], + "source": "session", + "cost_usd": 0.01, + "model": "claude-x" + } + ] +} diff --git a/dashboard/fixtures/data/projects/minimal-template/tasks.json b/dashboard/fixtures/data/projects/minimal-template/tasks.json new file mode 100644 index 0000000..75381da --- /dev/null +++ b/dashboard/fixtures/data/projects/minimal-template/tasks.json @@ -0,0 +1,14 @@ +{ + "tasks": [ + { + "id": "task-001", + "title": "Setup inicial", + "description": "template antigo do wrapper", + "status": "pending", + "max_attempts": 10, + "max_homologation_attempts": 5, + "skip_homologation": false, + "subtasks": [] + } + ] +} diff --git a/dashboard/fixtures/data/projects/squire-dashboard/checkpoint.json b/dashboard/fixtures/data/projects/squire-dashboard/checkpoint.json new file mode 100644 index 0000000..9f9f975 --- /dev/null +++ b/dashboard/fixtures/data/projects/squire-dashboard/checkpoint.json @@ -0,0 +1,34 @@ +{ + "version": 1, + "session_id": "sess-20260329-1000-abc123", + "phase": "implementing", + "started_at": "2026-03-29T09:00:00Z", + "last_heartbeat": "2026-03-29T10:15:00Z", + "cursor": { + "current_task_id": "task-003", + "current_subtask_id": null, + "step": "llm_execution", + "attempt": 3, + "homologation_attempt": 1 + }, + "llm_context": { + "last_instruction": "Fix src/lib/data.ts: getAlerts() must unwrap AlertList wrapper {alerts:[...]}", + "files_touched": ["src/lib/data.ts"], + "last_error": null, + "tests_passing": 3, + "tests_failing": 1, + "test_summary": "3 passed, 1 failed (AlertList unwrap)" + }, + "rate_limit": { + "claude_code_calls_this_window": 4, + "window_started_at": "2026-03-29T10:00:00Z", + "window_duration_minutes": 30, + "max_calls_per_window": 10 + }, + "recovery": { + "can_resume": true, + "resume_action": "continue", + "blocked_reason": null, + "escalation_needed": false + } +} diff --git a/dashboard/fixtures/data/projects/squire-dashboard/commits.json b/dashboard/fixtures/data/projects/squire-dashboard/commits.json new file mode 100644 index 0000000..6b2c669 --- /dev/null +++ b/dashboard/fixtures/data/projects/squire-dashboard/commits.json @@ -0,0 +1,18 @@ +{ + "commits": [ + { + "sha": "abc1234def56789", + "message": "feat: add project scaffold with Next.js App Router", + "timestamp": "2026-03-29T09:30:00Z", + "diff_summary": "Scaffold Next.js 14 com TypeScript, Tailwind CSS e estrutura de diretórios", + "files_changed": ["src/app/layout.tsx", "src/app/page.tsx", "package.json", "tailwind.config.ts"] + }, + { + "sha": "def5678abc12345", + "message": "feat: add TypeScript types mirroring squire Python models", + "timestamp": "2026-03-29T09:45:00Z", + "diff_summary": "Tipos TypeScript para Project, Task, HistoryEvent, Alert e GlobalStats", + "files_changed": ["src/lib/types.ts"] + } + ] +} diff --git a/dashboard/fixtures/data/projects/squire-dashboard/history.json b/dashboard/fixtures/data/projects/squire-dashboard/history.json new file mode 100644 index 0000000..1e90d90 --- /dev/null +++ b/dashboard/fixtures/data/projects/squire-dashboard/history.json @@ -0,0 +1,60 @@ +{ + "events": [ + { + "timestamp": "2026-03-29T09:00:00Z", + "type": "session_started", + "task_id": null, + "attempt": null, + "summary": "Sessão iniciada", + "actor": "squire" + }, + { + "timestamp": "2026-03-29T09:30:00Z", + "type": "task_completed", + "task_id": "task-001", + "attempt": 1, + "summary": "Scaffold concluído com sucesso", + "actor": "claude_code" + }, + { + "timestamp": "2026-03-29T09:45:00Z", + "type": "task_completed", + "task_id": "task-002", + "attempt": 1, + "summary": "Tipos TypeScript definidos", + "actor": "claude_code" + }, + { + "timestamp": "2026-03-29T10:00:00Z", + "type": "tests_passed", + "task_id": "task-003", + "attempt": 2, + "summary": "3 testes passando", + "actor": "local_llm" + }, + { + "timestamp": "2026-03-29T10:05:00Z", + "type": "homologation_requested", + "task_id": "task-003", + "attempt": 2, + "summary": "Homologação solicitada", + "actor": "squire" + }, + { + "timestamp": "2026-03-29T10:10:00Z", + "type": "homologation_failed", + "task_id": "task-003", + "attempt": 2, + "summary": "AlertList wrapper ausente", + "actor": "claude_code" + }, + { + "timestamp": "2026-03-29T10:15:00Z", + "type": "escalation_created", + "task_id": "task-003", + "attempt": 3, + "summary": "Padrão repetitivo detectado — escalação", + "actor": "squire" + } + ] +} diff --git a/dashboard/fixtures/data/projects/squire-dashboard/project.json b/dashboard/fixtures/data/projects/squire-dashboard/project.json new file mode 100644 index 0000000..effe02a --- /dev/null +++ b/dashboard/fixtures/data/projects/squire-dashboard/project.json @@ -0,0 +1,12 @@ +{ + "id": "squire-dashboard", + "name": "Squire Dashboard", + "description": "Dashboard Next.js para visualização em tempo real do estado do squire", + "repo_path": "/home/ai-debian/squire-dashboard", + "stack": ["nextjs", "typescript", "tailwind"], + "status": "implementing", + "created_at": "2026-03-29T00:00:00Z", + "updated_at": "2026-03-29T10:00:00Z", + "current_task_id": "task-003", + "coding_backend": "opencode" +} diff --git a/dashboard/fixtures/data/projects/squire-dashboard/tasks.json b/dashboard/fixtures/data/projects/squire-dashboard/tasks.json new file mode 100644 index 0000000..143ef7d --- /dev/null +++ b/dashboard/fixtures/data/projects/squire-dashboard/tasks.json @@ -0,0 +1,67 @@ +{ + "tasks": [ + { + "id": "task-001", + "title": "Scaffold Next.js project", + "description": "Initial project setup", + "status": "completed", + "assigned_to": "local_llm", + "attempts": 1, + "max_attempts": 10, + "homologation_result": "approved", + "homologation_attempt": 1, + "max_homologation_attempts": 5, + "completed_at": "2026-03-29T09:30:00Z", + "claude_code_assisted": false, + "subtasks": [], + "rejection_summaries": [], + "no_progress_streak": 0, + "skip_homologation": false, + "effort": "low", + "tdd": false, + "test_author": "claude" + }, + { + "id": "task-002", + "title": "Define TypeScript types", + "description": "Types mirroring Python models", + "status": "completed", + "assigned_to": "local_llm", + "attempts": 1, + "max_attempts": 10, + "homologation_result": "approved", + "homologation_attempt": 1, + "max_homologation_attempts": 5, + "completed_at": "2026-03-29T09:45:00Z", + "claude_code_assisted": false, + "subtasks": [], + "rejection_summaries": [], + "no_progress_streak": 0, + "skip_homologation": false, + "effort": "medium", + "tdd": false, + "test_author": "claude" + }, + { + "id": "task-003", + "title": "Data layer - JSON reading functions", + "description": "Functions to read squire JSON files", + "status": "implementing", + "assigned_to": "local_llm", + "attempts": 2, + "max_attempts": 10, + "homologation_result": null, + "homologation_attempt": 0, + "max_homologation_attempts": 5, + "completed_at": null, + "claude_code_assisted": false, + "subtasks": [], + "rejection_summaries": ["AlertList wrapper ausente"], + "no_progress_streak": 0, + "skip_homologation": false, + "effort": "medium", + "tdd": true, + "test_author": "claude" + } + ] +} diff --git a/dashboard/next.config.js b/dashboard/next.config.js new file mode 100644 index 0000000..b7f9dde --- /dev/null +++ b/dashboard/next.config.js @@ -0,0 +1,8 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + // Necessário para o Dockerfile multi-stage copiar apenas o standalone output + output: 'standalone', +}; + +module.exports = nextConfig; \ No newline at end of file diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json new file mode 100644 index 0000000..c786500 --- /dev/null +++ b/dashboard/package-lock.json @@ -0,0 +1,6036 @@ +{ + "name": "squire-dashboard", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "squire-dashboard", + "version": "0.1.0", + "dependencies": { + "lucide-react": "^1.7.0", + "next": "^14.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@playwright/test": "^1.60.0", + "@testing-library/jest-dom": "^6.4.0", + "@testing-library/react": "^16.0.0", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.0.1", + "jsdom": "^24.0.0", + "postcss": "^8", + "tailwindcss": "^3.3.0", + "typescript": "^5", + "vitest": "^1.6.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "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.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "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": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "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.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", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "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.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "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.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "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": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "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.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "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.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "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": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "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": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "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": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "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.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "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.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "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.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "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.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": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "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.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz", + "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz", + "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz", + "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz", + "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz", + "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz", + "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz", + "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz", + "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", + "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz", + "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@vitest/snapshot/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@vitest/utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.325", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.325.tgz", + "integrity": "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "24.1.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.3.tgz", + "integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.4", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.7.0.tgz", + "integrity": "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", + "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", + "license": "MIT", + "dependencies": { + "@next/env": "14.2.35", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.33", + "@next/swc-darwin-x64": "14.2.33", + "@next/swc-linux-arm64-gnu": "14.2.33", + "@next/swc-linux-arm64-musl": "14.2.33", + "@next/swc-linux-x64-gnu": "14.2.33", + "@next/swc-linux-x64-musl": "14.2.33", + "@next/swc-win32-arm64-msvc": "14.2.33", + "@next/swc-win32-ia32-msvc": "14.2.33", + "@next/swc-win32-x64-msvc": "14.2.33" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite-node/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "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": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/dashboard/package.json b/dashboard/package.json new file mode 100644 index 0000000..7cb6c2f --- /dev/null +++ b/dashboard/package.json @@ -0,0 +1,35 @@ +{ + "name": "squire-dashboard", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "test": "vitest run", + "test:watch": "vitest", + "test:e2e": "playwright test" + }, + "dependencies": { + "lucide-react": "^1.7.0", + "next": "^14.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@playwright/test": "^1.60.0", + "@testing-library/jest-dom": "^6.4.0", + "@testing-library/react": "^16.0.0", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.0.1", + "jsdom": "^24.0.0", + "postcss": "^8", + "tailwindcss": "^3.3.0", + "typescript": "^5", + "vitest": "^1.6.0" + } +} \ No newline at end of file diff --git a/dashboard/playwright.config.ts b/dashboard/playwright.config.ts new file mode 100644 index 0000000..24320b9 --- /dev/null +++ b/dashboard/playwright.config.ts @@ -0,0 +1,35 @@ +import { defineConfig } from '@playwright/test'; +import { join } from 'path'; + +/** + * Suite E2E: sobe `next dev` numa porta dedicada apontando para um estado + * seedado em e2e/.state (recriado pelo global-setup a cada run). Token de + * escrita fixo 'e2e-token'. Chromium only — ferramenta de LAN. + */ + +export const E2E_PORT = 3199; +export const E2E_TOKEN = 'e2e-token'; +export const E2E_STATE_DIR = join(__dirname, 'e2e', '.state'); + +export default defineConfig({ + testDir: './e2e', + globalSetup: './e2e/global-setup.ts', + timeout: 30_000, + retries: 0, + workers: 1, // estado compartilhado no filesystem — specs não podem competir + reporter: [['list']], + use: { + baseURL: `http://localhost:${E2E_PORT}`, + trace: 'retain-on-failure', + }, + webServer: { + command: `npx next dev -p ${E2E_PORT}`, + port: E2E_PORT, + reuseExistingServer: false, + timeout: 60_000, + env: { + SQUIRE_DATA_PATH: E2E_STATE_DIR, + DASHBOARD_WRITE_TOKEN: E2E_TOKEN, + }, + }, +}); diff --git a/dashboard/postcss.config.js b/dashboard/postcss.config.js new file mode 100644 index 0000000..8567b4c --- /dev/null +++ b/dashboard/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; \ No newline at end of file diff --git a/dashboard/public/.gitkeep b/dashboard/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dashboard/src/app/api/alerts/ack/route.test.ts b/dashboard/src/app/api/alerts/ack/route.test.ts new file mode 100644 index 0000000..ec3a608 --- /dev/null +++ b/dashboard/src/app/api/alerts/ack/route.test.ts @@ -0,0 +1,96 @@ +/** + * Padrão de teste de route handlers: + * squireStatePath.ts lê SQUIRE_DATA_PATH em module-load, então o env precisa + * ser stubado ANTES do import — por isso vi.resetModules() + import dinâmico. + */ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { NextRequest } from 'next/server'; + +const TOKEN = 'test-token'; + +function post(body: unknown, token: string | null = TOKEN): NextRequest { + const headers: Record = { 'Content-Type': 'application/json' }; + if (token) headers.Authorization = `Bearer ${token}`; + return new NextRequest('http://localhost/api/alerts/ack', { + method: 'POST', + headers, + body: JSON.stringify(body), + }); +} + +const ALERT = { + project_id: 'proj-a', + severity: 'critical', + type: 'max_homologations_reached', + task_id: 'task-001', + message: 'falhou', + created_at: '2026-06-11T00:00:00Z', + acknowledged: false, +}; + +describe('POST /api/alerts/ack', () => { + let dataDir: string; + let route: typeof import('./route'); + + beforeEach(async () => { + dataDir = mkdtempSync(join(tmpdir(), 'squire-dash-test-')); + writeFileSync(join(dataDir, 'alerts.json'), JSON.stringify({ alerts: [ALERT] })); + vi.stubEnv('SQUIRE_DATA_PATH', dataDir); + vi.stubEnv('DASHBOARD_WRITE_TOKEN', TOKEN); + vi.resetModules(); + route = await import('./route'); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + rmSync(dataDir, { recursive: true, force: true }); + }); + + it('exige token (401 sem Authorization)', async () => { + const res = await route.POST(post({ ...ALERT, dismiss: false }, null)); + expect(res.status).toBe(401); + }); + + it('responde 503 quando token não configurado no servidor', async () => { + vi.stubEnv('DASHBOARD_WRITE_TOKEN', ''); + const res = await route.POST(post({ ...ALERT, dismiss: false })); + expect(res.status).toBe(503); + }); + + it('ack marca acknowledged=true no arquivo', async () => { + const res = await route.POST( + post({ + project_id: ALERT.project_id, + task_id: ALERT.task_id, + created_at: ALERT.created_at, + }) + ); + expect(res.status).toBe(200); + const saved = JSON.parse(readFileSync(join(dataDir, 'alerts.json'), 'utf8')); + expect(saved.alerts[0].acknowledged).toBe(true); + }); + + it('dismiss remove o alerta', async () => { + const res = await route.POST( + post({ + project_id: ALERT.project_id, + task_id: ALERT.task_id, + created_at: ALERT.created_at, + dismiss: true, + }) + ); + expect(res.status).toBe(200); + const saved = JSON.parse(readFileSync(join(dataDir, 'alerts.json'), 'utf8')); + expect(saved.alerts).toHaveLength(0); + }); + + it('alerta inexistente devolve 404', async () => { + const res = await route.POST( + post({ project_id: 'x', task_id: null, created_at: '2020-01-01T00:00:00Z' }) + ); + expect(res.status).toBe(404); + }); +}); diff --git a/dashboard/src/app/api/alerts/ack/route.ts b/dashboard/src/app/api/alerts/ack/route.ts new file mode 100644 index 0000000..d42b7fe --- /dev/null +++ b/dashboard/src/app/api/alerts/ack/route.ts @@ -0,0 +1,67 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { readJson, writeJsonAtomic } from '@/lib/atomic'; +import { requireWriteToken } from '@/lib/auth'; +import { alertsPath } from '@/lib/squireStatePath'; +import type { AlertList } from '@/lib/types'; + +interface AckBody { + project_id: string; + task_id: string | null; + created_at: string; + dismiss?: boolean; +} + +function matchesAlert( + alert: AlertList['alerts'][number], + body: AckBody +): boolean { + return ( + alert.project_id === body.project_id && + (alert.task_id ?? null) === (body.task_id ?? null) && + alert.created_at === body.created_at + ); +} + +export async function POST(req: NextRequest) { + const denied = requireWriteToken(req); + if (denied) return denied; + + let body: AckBody; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: 'invalid_json' }, { status: 400 }); + } + + if (!body.project_id || !body.created_at) { + return NextResponse.json( + { error: 'missing_fields', required: ['project_id', 'created_at'] }, + { status: 400 } + ); + } + + const path = alertsPath(); + const list = (await readJson(path)) ?? { alerts: [] }; + + let updated = 0; + if (body.dismiss) { + const before = list.alerts.length; + list.alerts = list.alerts.filter((a) => !matchesAlert(a, body)); + updated = before - list.alerts.length; + } else { + list.alerts = list.alerts.map((a) => { + if (matchesAlert(a, body)) { + updated += 1; + return { ...a, acknowledged: true }; + } + return a; + }); + } + + if (updated === 0) { + return NextResponse.json({ error: 'alert_not_found' }, { status: 404 }); + } + + await writeJsonAtomic(path, list); + return NextResponse.json({ updated, dismissed: !!body.dismiss }); +} diff --git a/dashboard/src/app/api/auth/check/route.ts b/dashboard/src/app/api/auth/check/route.ts new file mode 100644 index 0000000..9c55e1b --- /dev/null +++ b/dashboard/src/app/api/auth/check/route.ts @@ -0,0 +1,9 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireWriteToken } from '@/lib/auth'; + +// Valida o token do usuário (usado pela página /login). +export async function GET(req: NextRequest) { + const denied = requireWriteToken(req); + if (denied) return denied; + return new NextResponse(null, { status: 204 }); +} diff --git a/dashboard/src/app/api/commands/[id]/route.test.ts b/dashboard/src/app/api/commands/[id]/route.test.ts new file mode 100644 index 0000000..758a106 --- /dev/null +++ b/dashboard/src/app/api/commands/[id]/route.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { NextRequest } from 'next/server'; + +const TOKEN = 'test-token'; +const UUID = '12345678-1234-1234-1234-123456789abc'; + +function get(id: string): NextRequest { + return new NextRequest(`http://localhost/api/commands/${id}`, { + headers: { Authorization: `Bearer ${TOKEN}` }, + }); +} + +describe('GET /api/commands/[id]', () => { + let dataDir: string; + let route: typeof import('./route'); + + beforeEach(async () => { + dataDir = mkdtempSync(join(tmpdir(), 'squire-dash-test-')); + for (const d of ['pending', 'running', 'done']) { + mkdirSync(join(dataDir, 'commands', d), { recursive: true }); + } + vi.stubEnv('SQUIRE_DATA_PATH', dataDir); + vi.stubEnv('DASHBOARD_WRITE_TOKEN', TOKEN); + vi.resetModules(); + route = await import('./route'); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + rmSync(dataDir, { recursive: true, force: true }); + }); + + it('400 para id fora do formato uuid (guarda de traversal)', async () => { + const res = await route.GET(get('..%2F..%2Fetc'), { + params: { id: '../../etc' }, + }); + expect(res.status).toBe(400); + }); + + it('404 para comando desconhecido', async () => { + const res = await route.GET(get(UUID), { params: { id: UUID } }); + expect(res.status).toBe(404); + }); + + it('reporta pending', async () => { + writeFileSync( + join(dataDir, 'commands', 'pending', `${UUID}.json`), + JSON.stringify({ id: UUID, type: 'run', project_id: 'p', args: {} }) + ); + const res = await route.GET(get(UUID), { params: { id: UUID } }); + const body = await res.json(); + expect(body.status).toBe('pending'); + }); + + it('reporta done com o resultado', async () => { + writeFileSync( + join(dataDir, 'commands', 'done', `${UUID}.json`), + JSON.stringify({ + id: UUID, + type: 'run', + project_id: 'p', + status: 'done', + exit_code: 0, + stdout_tail: 'ok', + stderr_tail: '', + started_at: null, + finished_at: null, + error: null, + }) + ); + const res = await route.GET(get(UUID), { params: { id: UUID } }); + const body = await res.json(); + expect(body.status).toBe('done'); + expect(body.result.exit_code).toBe(0); + }); +}); diff --git a/dashboard/src/app/api/commands/[id]/route.ts b/dashboard/src/app/api/commands/[id]/route.ts new file mode 100644 index 0000000..cba6ad9 --- /dev/null +++ b/dashboard/src/app/api/commands/[id]/route.ts @@ -0,0 +1,26 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireWriteToken } from '@/lib/auth'; +import { getCommandStatus } from '@/lib/commands'; + +// UUID v4 — também serve de guarda contra path traversal no nome do arquivo +const ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; + +export async function GET( + req: NextRequest, + { params }: { params: { id: string } } +) { + // Resultados carregam stdout do host — exigem o mesmo token das escritas + const denied = requireWriteToken(req); + if (denied) return denied; + + if (!ID_RE.test(params.id)) { + return NextResponse.json({ error: 'invalid_id' }, { status: 400 }); + } + + const view = await getCommandStatus(params.id); + if (!view) { + // desconhecido ou resultado expirado pelo TTL do agente + return NextResponse.json({ error: 'not_found' }, { status: 404 }); + } + return NextResponse.json(view); +} diff --git a/dashboard/src/app/api/commands/route.test.ts b/dashboard/src/app/api/commands/route.test.ts new file mode 100644 index 0000000..3f1f216 --- /dev/null +++ b/dashboard/src/app/api/commands/route.test.ts @@ -0,0 +1,147 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { NextRequest } from 'next/server'; + +const TOKEN = 'test-token'; + +function post(body: unknown, token: string | null = TOKEN): NextRequest { + const headers: Record = { 'Content-Type': 'application/json' }; + if (token) headers.Authorization = `Bearer ${token}`; + return new NextRequest('http://localhost/api/commands', { + method: 'POST', + headers, + body: JSON.stringify(body), + }); +} + +describe('POST /api/commands', () => { + let dataDir: string; + let route: typeof import('./route'); + + beforeEach(async () => { + dataDir = mkdtempSync(join(tmpdir(), 'squire-dash-test-')); + vi.stubEnv('SQUIRE_DATA_PATH', dataDir); + vi.stubEnv('DASHBOARD_WRITE_TOKEN', TOKEN); + vi.resetModules(); + route = await import('./route'); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + rmSync(dataDir, { recursive: true, force: true }); + }); + + it('401 sem token', async () => { + const res = await route.POST(post({ type: 'run', project_id: 'p' }, null)); + expect(res.status).toBe(401); + }); + + it('enfileira comando válido em pending/ com shape correto', async () => { + const res = await route.POST( + post({ + type: 'plan_tasks', + project_id: 'meu-app', + args: { description: 'API', mode: 'append' }, + }) + ); + expect(res.status).toBe(202); + const { id } = await res.json(); + const files = readdirSync(join(dataDir, 'commands', 'pending')); + expect(files).toEqual([`${id}.json`]); + const cmd = JSON.parse( + readFileSync(join(dataDir, 'commands', 'pending', files[0]), 'utf8') + ); + expect(cmd).toMatchObject({ + id, + type: 'plan_tasks', + project_id: 'meu-app', + args: { description: 'API', mode: 'append' }, + requested_by: 'dashboard', + }); + expect(cmd.created_at).toBeTruthy(); + }); + + it('400 em type desconhecido', async () => { + const res = await route.POST(post({ type: 'rm_rf', project_id: 'p' })); + expect(res.status).toBe(400); + }); + + it('400 em project_id inválido', async () => { + const res = await route.POST(post({ type: 'run', project_id: '../etc' })); + expect(res.status).toBe(400); + }); + + it('400 em mode inválido para plan_tasks', async () => { + const res = await route.POST( + post({ type: 'plan_tasks', project_id: 'p', args: { mode: 'destroy' } }) + ); + expect(res.status).toBe(400); + }); + + it('409 para run com sessão ativa', async () => { + writeFileSync( + join(dataDir, 'session.lock'), + JSON.stringify({ + holder: 'sess-x', + acquired_at: new Date().toISOString(), + ttl_minutes: 60, + pid: process.pid, + }) + ); + const res = await route.POST(post({ type: 'run', project_id: 'meu-app' })); + expect(res.status).toBe(409); + }); + + it('kill não exige project_id', async () => { + const res = await route.POST(post({ type: 'kill' })); + expect(res.status).toBe(202); + }); +}); + +describe('POST /api/commands — fix_task', () => { + let dataDir: string; + let route: typeof import('./route'); + + beforeEach(async () => { + dataDir = mkdtempSync(join(tmpdir(), 'squire-dash-test-')); + vi.stubEnv('SQUIRE_DATA_PATH', dataDir); + vi.stubEnv('DASHBOARD_WRITE_TOKEN', TOKEN); + vi.resetModules(); + route = await import('./route'); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + rmSync(dataDir, { recursive: true, force: true }); + }); + + it('enfileira fix_task válido', async () => { + const res = await route.POST( + post({ type: 'fix_task', project_id: 'proj', args: { task_id: 'task-009' } }) + ); + expect(res.status).toBe(202); + }); + + it('400 sem task_id', async () => { + const res = await route.POST(post({ type: 'fix_task', project_id: 'proj' })); + expect(res.status).toBe(400); + }); + + it('409 com sessão ativa (fix segura o lock)', async () => { + writeFileSync( + join(dataDir, 'session.lock'), + JSON.stringify({ + holder: 'sess-x', + acquired_at: new Date().toISOString(), + ttl_minutes: 60, + pid: process.pid, + }) + ); + const res = await route.POST( + post({ type: 'fix_task', project_id: 'proj', args: { task_id: 'task-009' } }) + ); + expect(res.status).toBe(409); + }); +}); diff --git a/dashboard/src/app/api/commands/route.ts b/dashboard/src/app/api/commands/route.ts new file mode 100644 index 0000000..a3dfcc8 --- /dev/null +++ b/dashboard/src/app/api/commands/route.ts @@ -0,0 +1,101 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { requireWriteToken } from '@/lib/auth'; +import { enqueueCommand } from '@/lib/commands'; +import { readSessionLock } from '@/lib/squireLock'; +import type { CommandType } from '@/lib/types'; + +/** + * Enfileira um comando para o agente host. A validação aqui espelha a do + * agente (que valida de novo — não confiamos só no dashboard); o objetivo + * é falhar cedo com mensagens claras na UI. + */ + +const COMMAND_TYPES: CommandType[] = [ + 'new_project', + 'run', + 'resume', + 'kill', + 'plan_tasks', + 'split_task', + 'fix_task', +]; +const PROJECT_ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/; +const TASK_ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/; +const BACKENDS = ['opencode', 'litellm', 'crush']; + +interface EnqueueBody { + type?: CommandType; + project_id?: string | null; + args?: Record; +} + +function validationError(body: EnqueueBody): string | null { + const { type, project_id: projectId, args = {} } = body; + + if (!type || !COMMAND_TYPES.includes(type)) { + return `type deve ser um de: ${COMMAND_TYPES.join(', ')}`; + } + if (type !== 'kill') { + if (!projectId || !PROJECT_ID_RE.test(projectId)) { + return 'project_id inválido (minúsculas, dígitos e hífens)'; + } + } + if (type === 'new_project') { + const backend = (args.backend as string) ?? 'opencode'; + if (!BACKENDS.includes(backend)) { + return `backend deve ser um de: ${BACKENDS.join(', ')}`; + } + } + if (type === 'plan_tasks') { + const mode = (args.mode as string) ?? 'append'; + if (!['append', 'replace'].includes(mode)) { + return "mode deve ser 'append' ou 'replace'"; + } + } + if (type === 'split_task' || type === 'fix_task') { + if (!TASK_ID_RE.test((args.task_id as string) ?? '')) { + return 'task_id inválido'; + } + } + return null; +} + +export async function POST(req: NextRequest) { + const denied = requireWriteToken(req); + if (denied) return denied; + + let body: EnqueueBody; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: 'invalid_json' }, { status: 400 }); + } + + const problem = validationError(body); + if (problem) { + return NextResponse.json({ error: 'invalid_command', message: problem }, { status: 400 }); + } + + // Pré-check de lock para run/resume/fix: melhor 409 imediato na UI do que + // um comando que falha no agente segundos depois (o fix segura o lock). + if (body.type === 'run' || body.type === 'resume' || body.type === 'fix_task') { + const lock = await readSessionLock(); + if (lock.held) { + return NextResponse.json( + { + error: 'squire_running', + message: `Já existe uma sessão ativa (${lock.holder}). Espere terminar ou use Kill.`, + holder: lock.holder, + }, + { status: 409 } + ); + } + } + + const cmd = await enqueueCommand( + body.type!, + body.type === 'kill' ? null : body.project_id!, + body.args ?? {} + ); + return NextResponse.json({ id: cmd.id }, { status: 202 }); +} diff --git a/dashboard/src/app/api/health/route.ts b/dashboard/src/app/api/health/route.ts new file mode 100644 index 0000000..2b73370 --- /dev/null +++ b/dashboard/src/app/api/health/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from 'next/server'; + +// Sem request object o Next otimiza o handler para estático e congela o +// timestamp do build — force dynamic para o healthcheck ser real. +export const dynamic = 'force-dynamic'; + +export async function GET() { + // Endpoint de saúde para teste de polling + return NextResponse.json({ + status: 'ok', + timestamp: new Date().toISOString(), + message: 'Servidor saudável' + }); +} \ No newline at end of file diff --git a/dashboard/src/app/api/projects/[id]/budget/route.ts b/dashboard/src/app/api/projects/[id]/budget/route.ts new file mode 100644 index 0000000..06bd2f1 --- /dev/null +++ b/dashboard/src/app/api/projects/[id]/budget/route.ts @@ -0,0 +1,51 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { readJson, writeJsonAtomic } from '@/lib/atomic'; +import { guardProjectWrite } from '@/lib/auth'; +import { checkpointPath } from '@/lib/squireStatePath'; +import type { Checkpoint } from '@/lib/types'; + +interface BudgetBody { + max_daily_usd?: number; + max_calls_per_window?: number; +} + +export async function POST( + req: NextRequest, + { params }: { params: { id: string } } +) { + const denied = await guardProjectWrite(req, params.id); + if (denied) return denied; + + let body: BudgetBody; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: 'invalid_json' }, { status: 400 }); + } + + const path = checkpointPath(params.id); + const cp = await readJson(path); + if (!cp) { + return NextResponse.json({ error: 'checkpoint_not_found' }, { status: 404 }); + } + + const patch: string[] = []; + if (typeof body.max_daily_usd === 'number' && body.max_daily_usd >= 0) { + cp.rate_limit.max_daily_usd = body.max_daily_usd; + patch.push('max_daily_usd'); + } + if (typeof body.max_calls_per_window === 'number' && body.max_calls_per_window > 0) { + cp.rate_limit.max_calls_per_window = body.max_calls_per_window; + patch.push('max_calls_per_window'); + } + + if (patch.length === 0) { + return NextResponse.json( + { error: 'no_valid_fields', accepts: ['max_daily_usd', 'max_calls_per_window'] }, + { status: 400 } + ); + } + + await writeJsonAtomic(path, cp); + return NextResponse.json({ updated: patch, rate_limit: cp.rate_limit }); +} diff --git a/dashboard/src/app/api/projects/[id]/route.test.ts b/dashboard/src/app/api/projects/[id]/route.test.ts new file mode 100644 index 0000000..48a16c5 --- /dev/null +++ b/dashboard/src/app/api/projects/[id]/route.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { NextRequest } from 'next/server'; + +const TOKEN = 'test-token'; +const PROJECT = 'proj-a'; + +const PROJECT_JSON = { + id: PROJECT, + name: 'Proj A', + description: '', + repo_path: '/tmp/proj-a', + stack: ['python'], + status: 'planning', + created_at: '2026-06-01T00:00:00Z', + updated_at: '2026-06-01T00:00:00Z', + current_task_id: null, + coding_backend: 'opencode', +}; + +function patch(body: unknown): NextRequest { + return new NextRequest(`http://localhost/api/projects/${PROJECT}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${TOKEN}`, + }, + body: JSON.stringify(body), + }); +} + +describe('PATCH /api/projects/[id]', () => { + let dataDir: string; + let route: typeof import('./route'); + + beforeEach(async () => { + dataDir = mkdtempSync(join(tmpdir(), 'squire-dash-test-')); + mkdirSync(join(dataDir, 'projects', PROJECT), { recursive: true }); + writeFileSync( + join(dataDir, 'projects', PROJECT, 'project.json'), + JSON.stringify(PROJECT_JSON) + ); + vi.stubEnv('SQUIRE_DATA_PATH', dataDir); + vi.stubEnv('DASHBOARD_WRITE_TOKEN', TOKEN); + vi.resetModules(); + route = await import('./route'); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + rmSync(dataDir, { recursive: true, force: true }); + }); + + const params = { params: { id: PROJECT } }; + + const saved = () => + JSON.parse( + readFileSync(join(dataDir, 'projects', PROJECT, 'project.json'), 'utf8') + ); + + it('atualiza campos da whitelist e bump em updated_at', async () => { + const res = await route.PATCH( + patch({ + name: 'Renomeado', + description: 'API REST', + stack: ['python', ' flask '], + coding_backend: 'litellm', + status: 'implementing', + }), + params + ); + expect(res.status).toBe(200); + const p = saved(); + expect(p.name).toBe('Renomeado'); + expect(p.stack).toEqual(['python', 'flask']); + expect(p.coding_backend).toBe('litellm'); + expect(p.status).toBe('implementing'); + expect(p.updated_at).not.toBe(PROJECT_JSON.updated_at); + expect(p.repo_path).toBe(PROJECT_JSON.repo_path); // intocado + }); + + it('400 em backend inválido', async () => { + const res = await route.PATCH(patch({ coding_backend: 'aider' }), params); + expect(res.status).toBe(400); + }); + + it('400 em status inválido', async () => { + const res = await route.PATCH(patch({ status: 'voando' }), params); + expect(res.status).toBe(400); + }); + + it('404 em projeto inexistente', async () => { + const res = await route.PATCH(patch({ name: 'X' }), { + params: { id: 'nao-existe' }, + }); + expect(res.status).toBe(404); + }); +}); diff --git a/dashboard/src/app/api/projects/[id]/route.ts b/dashboard/src/app/api/projects/[id]/route.ts new file mode 100644 index 0000000..91bcf45 --- /dev/null +++ b/dashboard/src/app/api/projects/[id]/route.ts @@ -0,0 +1,92 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { readJson, writeJsonAtomic } from '@/lib/atomic'; +import { guardProjectWrite } from '@/lib/auth'; +import { projectJsonPath } from '@/lib/squireStatePath'; +import type { Project, ProjectStatus } from '@/lib/types'; + +const PROJECT_STATUSES: ProjectStatus[] = [ + 'planning', + 'implementing', + 'reviewing', + 'blocked', + 'completed', +]; +const BACKENDS = ['opencode', 'litellm', 'crush']; + +interface PatchProjectBody { + name?: string; + description?: string; + stack?: string[]; + coding_backend?: string | null; + status?: string; +} + +export async function PATCH( + req: NextRequest, + { params }: { params: { id: string } } +) { + const denied = await guardProjectWrite(req, params.id); + if (denied) return denied; + + let body: PatchProjectBody; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: 'invalid_json' }, { status: 400 }); + } + + const path = projectJsonPath(params.id); + const project = await readJson(path); + if (!project) { + return NextResponse.json({ error: 'project_not_found' }, { status: 404 }); + } + + const applied: string[] = []; + + if (typeof body.name === 'string' && body.name.trim()) { + project.name = body.name.trim(); + applied.push('name'); + } + if (typeof body.description === 'string') { + project.description = body.description; + applied.push('description'); + } + if (Array.isArray(body.stack) && body.stack.every((s) => typeof s === 'string')) { + project.stack = body.stack.map((s) => s.trim()).filter(Boolean); + applied.push('stack'); + } + if (body.coding_backend !== undefined) { + if (body.coding_backend !== null && !BACKENDS.includes(body.coding_backend)) { + return NextResponse.json( + { error: 'invalid_backend', accepts: BACKENDS }, + { status: 400 } + ); + } + project.coding_backend = body.coding_backend; + applied.push('coding_backend'); + } + if (body.status !== undefined) { + if (!PROJECT_STATUSES.includes(body.status as ProjectStatus)) { + return NextResponse.json( + { error: 'invalid_status', accepts: PROJECT_STATUSES }, + { status: 400 } + ); + } + project.status = body.status as ProjectStatus; + applied.push('status'); + } + + if (applied.length === 0) { + return NextResponse.json( + { + error: 'no_valid_fields', + accepts: ['name', 'description', 'stack', 'coding_backend', 'status'], + }, + { status: 400 } + ); + } + + project.updated_at = new Date().toISOString(); + await writeJsonAtomic(path, project); + return NextResponse.json({ updated: applied, project }); +} diff --git a/dashboard/src/app/api/projects/[id]/tasks/[taskId]/action/route.ts b/dashboard/src/app/api/projects/[id]/tasks/[taskId]/action/route.ts new file mode 100644 index 0000000..67b57fa --- /dev/null +++ b/dashboard/src/app/api/projects/[id]/tasks/[taskId]/action/route.ts @@ -0,0 +1,83 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { readJson, writeJsonAtomic } from '@/lib/atomic'; +import { guardProjectWrite } from '@/lib/auth'; +import { tasksPath } from '@/lib/squireStatePath'; +import type { TaskList, Task } from '@/lib/types'; + +type ActionKind = 'retry' | 'approve' | 'skip'; + +interface ActionBody { + action: ActionKind; +} + +function applyAction(task: Task, action: ActionKind): Task { + if (action === 'retry') { + return { + ...task, + attempts: 0, + homologation_attempt: 0, + homologation_result: null, + no_progress_streak: 0, + rejection_summaries: [], + status: 'pending', + }; + } + if (action === 'approve') { + return { + ...task, + homologation_result: 'approved', + status: 'completed', + completed_at: new Date().toISOString(), + }; + } + // skip + return { + ...task, + skip_homologation: true, + status: 'pending', + }; +} + +export async function POST( + req: NextRequest, + { params }: { params: { id: string; taskId: string } } +) { + const { id: projectId, taskId } = params; + + const denied = await guardProjectWrite(req, projectId); + if (denied) return denied; + + let body: ActionBody; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: 'invalid_json' }, { status: 400 }); + } + + if (!body.action || !['retry', 'approve', 'skip'].includes(body.action)) { + return NextResponse.json( + { error: 'invalid_action', accepts: ['retry', 'approve', 'skip'] }, + { status: 400 } + ); + } + + const path = tasksPath(projectId); + const list = await readJson(path); + if (!list) { + return NextResponse.json({ error: 'tasks_not_found' }, { status: 404 }); + } + + let touched = false; + list.tasks = list.tasks.map((t) => { + if (t.id !== taskId) return t; + touched = true; + return applyAction(t, body.action); + }); + + if (!touched) { + return NextResponse.json({ error: 'task_not_found', task_id: taskId }, { status: 404 }); + } + + await writeJsonAtomic(path, list); + return NextResponse.json({ action: body.action, task_id: taskId }); +} diff --git a/dashboard/src/app/api/projects/[id]/tasks/[taskId]/route.test.ts b/dashboard/src/app/api/projects/[id]/tasks/[taskId]/route.test.ts new file mode 100644 index 0000000..302068c --- /dev/null +++ b/dashboard/src/app/api/projects/[id]/tasks/[taskId]/route.test.ts @@ -0,0 +1,119 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { NextRequest } from 'next/server'; + +const TOKEN = 'test-token'; +const PROJECT = 'proj-a'; + +const TASK = { + id: 'task-001', + title: 'Original', + description: '', + status: 'blocked', + assigned_to: 'local_llm', + attempts: 7, + max_attempts: 10, + homologation_result: 'rejected', + homologation_attempt: 5, + max_homologation_attempts: 5, + completed_at: null, + claude_code_assisted: false, + subtasks: [], + rejection_summaries: ['r1'], + no_progress_streak: 0, + skip_homologation: false, + effort: 'medium', + tdd: true, + test_author: 'claude', + max_usd: null, + cost_usd: 1.2, +}; + +function reqFor(method: string, body?: unknown): NextRequest { + return new NextRequest( + `http://localhost/api/projects/${PROJECT}/tasks/task-001`, + { + method, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${TOKEN}`, + }, + body: body === undefined ? undefined : JSON.stringify(body), + } + ); +} + +describe('PATCH/DELETE /api/projects/[id]/tasks/[taskId]', () => { + let dataDir: string; + let route: typeof import('./route'); + + beforeEach(async () => { + dataDir = mkdtempSync(join(tmpdir(), 'squire-dash-test-')); + mkdirSync(join(dataDir, 'projects', PROJECT), { recursive: true }); + writeFileSync( + join(dataDir, 'projects', PROJECT, 'tasks.json'), + JSON.stringify({ tasks: [TASK] }) + ); + vi.stubEnv('SQUIRE_DATA_PATH', dataDir); + vi.stubEnv('DASHBOARD_WRITE_TOKEN', TOKEN); + vi.resetModules(); + route = await import('./route'); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + rmSync(dataDir, { recursive: true, force: true }); + }); + + const params = { params: { id: PROJECT, taskId: 'task-001' } }; + + const savedTasks = () => + JSON.parse( + readFileSync(join(dataDir, 'projects', PROJECT, 'tasks.json'), 'utf8') + ).tasks; + + it('PATCH aplica apenas campos da whitelist', async () => { + const res = await route.PATCH( + reqFor('PATCH', { + title: 'Editado', + effort: 'high', + status: 'completed', // fora da whitelist — ignorado + attempts: 0, // fora da whitelist — ignorado + }), + params + ); + expect(res.status).toBe(200); + const t = savedTasks()[0]; + expect(t.title).toBe('Editado'); + expect(t.effort).toBe('high'); + expect(t.status).toBe('blocked'); // intocado + expect(t.attempts).toBe(7); // intocado + }); + + it('PATCH sem campos válidos retorna 400', async () => { + const res = await route.PATCH(reqFor('PATCH', { attempts: 0 }), params); + expect(res.status).toBe(400); + }); + + it('PATCH em task inexistente retorna 404', async () => { + const res = await route.PATCH(reqFor('PATCH', { title: 'X' }), { + params: { id: PROJECT, taskId: 'task-999' }, + }); + expect(res.status).toBe(404); + }); + + it('DELETE remove a task', async () => { + const res = await route.DELETE(reqFor('DELETE'), params); + expect(res.status).toBe(200); + expect(savedTasks()).toHaveLength(0); + }); + + it('DELETE de task inexistente retorna 404', async () => { + const res = await route.DELETE(reqFor('DELETE'), { + params: { id: PROJECT, taskId: 'task-999' }, + }); + expect(res.status).toBe(404); + }); +}); diff --git a/dashboard/src/app/api/projects/[id]/tasks/[taskId]/route.ts b/dashboard/src/app/api/projects/[id]/tasks/[taskId]/route.ts new file mode 100644 index 0000000..a9ba1fe --- /dev/null +++ b/dashboard/src/app/api/projects/[id]/tasks/[taskId]/route.ts @@ -0,0 +1,84 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { readJson, writeJsonAtomic } from '@/lib/atomic'; +import { guardProjectWrite } from '@/lib/auth'; +import { EDITABLE_TASK_FIELDS, EFFORTS, TEST_AUTHORS } from '@/lib/taskDefaults'; +import { tasksPath } from '@/lib/squireStatePath'; +import type { Task, TaskList } from '@/lib/types'; + +export async function PATCH( + req: NextRequest, + { params }: { params: { id: string; taskId: string } } +) { + const denied = await guardProjectWrite(req, params.id); + if (denied) return denied; + + let body: Partial; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: 'invalid_json' }, { status: 400 }); + } + + if (body.effort !== undefined && !EFFORTS.includes(body.effort)) { + return NextResponse.json({ error: 'invalid_effort', accepts: EFFORTS }, { status: 400 }); + } + if (body.test_author !== undefined && !TEST_AUTHORS.includes(body.test_author)) { + return NextResponse.json( + { error: 'invalid_test_author', accepts: TEST_AUTHORS }, + { status: 400 } + ); + } + + const path = tasksPath(params.id); + const taskList = await readJson(path); + if (!taskList) { + return NextResponse.json({ error: 'project_not_found' }, { status: 404 }); + } + const task = taskList.tasks.find((t) => t.id === params.taskId); + if (!task) { + return NextResponse.json({ error: 'task_not_found' }, { status: 404 }); + } + + // Aplica somente a whitelist — status/attempts/cost ficam intocados + const applied: string[] = []; + const target = task as unknown as Record; + for (const field of EDITABLE_TASK_FIELDS) { + if (field in body && body[field] !== undefined) { + target[field] = body[field]; + applied.push(field); + } + } + + if (applied.length === 0) { + return NextResponse.json( + { error: 'no_valid_fields', accepts: EDITABLE_TASK_FIELDS }, + { status: 400 } + ); + } + + await writeJsonAtomic(path, taskList); + return NextResponse.json({ updated: applied, task }); +} + +export async function DELETE( + req: NextRequest, + { params }: { params: { id: string; taskId: string } } +) { + const denied = await guardProjectWrite(req, params.id); + if (denied) return denied; + + const path = tasksPath(params.id); + const taskList = await readJson(path); + if (!taskList) { + return NextResponse.json({ error: 'project_not_found' }, { status: 404 }); + } + + const before = taskList.tasks.length; + taskList.tasks = taskList.tasks.filter((t) => t.id !== params.taskId); + if (taskList.tasks.length === before) { + return NextResponse.json({ error: 'task_not_found' }, { status: 404 }); + } + + await writeJsonAtomic(path, taskList); + return NextResponse.json({ deleted: params.taskId }); +} diff --git a/dashboard/src/app/api/projects/[id]/tasks/route.test.ts b/dashboard/src/app/api/projects/[id]/tasks/route.test.ts new file mode 100644 index 0000000..ae9f0c6 --- /dev/null +++ b/dashboard/src/app/api/projects/[id]/tasks/route.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { NextRequest } from 'next/server'; + +const TOKEN = 'test-token'; +const PROJECT = 'proj-a'; + +function post(body: unknown, token: string | null = TOKEN): NextRequest { + const headers: Record = { 'Content-Type': 'application/json' }; + if (token) headers.Authorization = `Bearer ${token}`; + return new NextRequest(`http://localhost/api/projects/${PROJECT}/tasks`, { + method: 'POST', + headers, + body: JSON.stringify(body), + }); +} + +describe('POST /api/projects/[id]/tasks', () => { + let dataDir: string; + let route: typeof import('./route'); + + beforeEach(async () => { + dataDir = mkdtempSync(join(tmpdir(), 'squire-dash-test-')); + mkdirSync(join(dataDir, 'projects', PROJECT), { recursive: true }); + writeFileSync( + join(dataDir, 'projects', PROJECT, 'tasks.json'), + JSON.stringify({ tasks: [] }) + ); + vi.stubEnv('SQUIRE_DATA_PATH', dataDir); + vi.stubEnv('DASHBOARD_WRITE_TOKEN', TOKEN); + vi.resetModules(); + route = await import('./route'); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + rmSync(dataDir, { recursive: true, force: true }); + }); + + const params = { params: { id: PROJECT } }; + + it('401 sem token', async () => { + const res = await route.POST(post({ title: 'X' }, null), params); + expect(res.status).toBe(401); + }); + + it('cria task com defaults do modelo Python', async () => { + const res = await route.POST(post({ title: 'Nova' }), params); + expect(res.status).toBe(201); + const saved = JSON.parse( + readFileSync(join(dataDir, 'projects', PROJECT, 'tasks.json'), 'utf8') + ); + expect(saved.tasks).toHaveLength(1); + const t = saved.tasks[0]; + expect(t.id).toBe('task-001'); + expect(t.status).toBe('pending'); + expect(t.max_attempts).toBe(10); + expect(t.tdd).toBe(true); + expect(t.effort).toBe('medium'); + }); + + it('409 em id duplicado', async () => { + await route.POST(post({ title: 'A', id: 'task-001' }), params); + const res = await route.POST(post({ title: 'B', id: 'task-001' }), params); + expect(res.status).toBe(409); + }); + + it('400 sem título', async () => { + const res = await route.POST(post({ description: 'sem título' }), params); + expect(res.status).toBe(400); + }); + + it('400 com effort inválido', async () => { + const res = await route.POST(post({ title: 'X', effort: 'épico' }), params); + expect(res.status).toBe(400); + }); + + it('404 para projeto inexistente', async () => { + const res = await route.POST(post({ title: 'X' }), { + params: { id: 'nao-existe' }, + }); + expect(res.status).toBe(404); + }); + + it('409 quando squire roda o projeto (session lock)', async () => { + writeFileSync( + join(dataDir, 'session.lock'), + JSON.stringify({ + holder: `sess-x-${PROJECT}`, + project_id: PROJECT, + acquired_at: new Date().toISOString(), + ttl_minutes: 60, + pid: process.pid, + }) + ); + const res = await route.POST(post({ title: 'X' }), params); + expect(res.status).toBe(409); + }); +}); diff --git a/dashboard/src/app/api/projects/[id]/tasks/route.ts b/dashboard/src/app/api/projects/[id]/tasks/route.ts new file mode 100644 index 0000000..ebc88ae --- /dev/null +++ b/dashboard/src/app/api/projects/[id]/tasks/route.ts @@ -0,0 +1,84 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { readJson, writeJsonAtomic } from '@/lib/atomic'; +import { guardProjectWrite } from '@/lib/auth'; +import { newTask, nextTaskId, EFFORTS, TEST_AUTHORS } from '@/lib/taskDefaults'; +import { tasksPath } from '@/lib/squireStatePath'; +import type { TaskList } from '@/lib/types'; + +interface CreateTaskBody { + id?: string; + title?: string; + description?: string; + effort?: string; + tdd?: boolean; + test_author?: string; + skip_homologation?: boolean; + max_attempts?: number; + max_homologation_attempts?: number; + max_usd?: number | null; +} + +export async function POST( + req: NextRequest, + { params }: { params: { id: string } } +) { + const denied = await guardProjectWrite(req, params.id); + if (denied) return denied; + + let body: CreateTaskBody; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: 'invalid_json' }, { status: 400 }); + } + + if (!body.title || !body.title.trim()) { + return NextResponse.json( + { error: 'missing_fields', required: ['title'] }, + { status: 400 } + ); + } + if (body.effort !== undefined && !EFFORTS.includes(body.effort as never)) { + return NextResponse.json( + { error: 'invalid_effort', accepts: EFFORTS }, + { status: 400 } + ); + } + if ( + body.test_author !== undefined && + !TEST_AUTHORS.includes(body.test_author as never) + ) { + return NextResponse.json( + { error: 'invalid_test_author', accepts: TEST_AUTHORS }, + { status: 400 } + ); + } + + const path = tasksPath(params.id); + const taskList = (await readJson(path)) ?? null; + if (!taskList) { + return NextResponse.json({ error: 'project_not_found' }, { status: 404 }); + } + + const id = body.id?.trim() || nextTaskId(taskList); + if (taskList.tasks.some((t) => t.id === id)) { + return NextResponse.json({ error: 'duplicate_id', id }, { status: 409 }); + } + + const task = newTask({ + id, + title: body.title.trim(), + description: body.description, + effort: body.effort as never, + tdd: body.tdd, + test_author: body.test_author as never, + skip_homologation: body.skip_homologation, + max_attempts: body.max_attempts, + max_homologation_attempts: body.max_homologation_attempts, + max_usd: body.max_usd, + }); + + taskList.tasks.push(task); + await writeJsonAtomic(path, taskList); + return NextResponse.json({ created: task }, { status: 201 }); +} diff --git a/dashboard/src/app/dashboard/page.tsx b/dashboard/src/app/dashboard/page.tsx new file mode 100644 index 0000000..7e22930 --- /dev/null +++ b/dashboard/src/app/dashboard/page.tsx @@ -0,0 +1,6 @@ +import { redirect } from 'next/navigation'; + +// Rota legada — redireciona para a raiz +export default function DashboardPage() { + redirect('/'); +} diff --git a/dashboard/src/app/error.tsx b/dashboard/src/app/error.tsx new file mode 100644 index 0000000..d12ae25 --- /dev/null +++ b/dashboard/src/app/error.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { AlertTriangle } from 'lucide-react'; + +export default function GlobalError({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + return ( +
+
+ +

+ Algo quebrou ao renderizar esta página +

+

+ {error.message || 'Erro inesperado.'} + {error.digest && ( + + digest: {error.digest} + + )} +

+

+ Geralmente é um JSON de estado inválido — verifique os arquivos do + projeto em squire-state ou rode `squire doctor`. +

+ +
+
+ ); +} diff --git a/dashboard/src/app/globals.css b/dashboard/src/app/globals.css new file mode 100644 index 0000000..82bae32 --- /dev/null +++ b/dashboard/src/app/globals.css @@ -0,0 +1,33 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } +} + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient( + to bottom, + transparent, + rgb(var(--background-end-rgb)) + ) + rgb(var(--background-start-rgb)); +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} \ No newline at end of file diff --git a/dashboard/src/app/layout.tsx b/dashboard/src/app/layout.tsx new file mode 100644 index 0000000..64a70b7 --- /dev/null +++ b/dashboard/src/app/layout.tsx @@ -0,0 +1,39 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; +import Sidebar from "@/components/Sidebar"; +import HealthStrip from "@/components/HealthStrip"; + +const inter = Inter({ subsets: ["latin"] }); + +// O chrome do layout (Sidebar + HealthStrip) lê o estado do squire no +// filesystem a cada render. O build roda SEM o volume de dados — qualquer +// rota prerenderizada congelaria sidebar vazia e sessão falsa (já mordeu +// em /, /api/health e /projects/new). force-dynamic aqui se aplica a +// todas as páginas; route handlers precisam do export próprio. +export const dynamic = "force-dynamic"; + +export const metadata: Metadata = { + title: "Squire Dashboard", + description: "Visualização em tempo real do estado do squire", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + +
+ +
+ + {children} +
+
+ + + ); +} diff --git a/dashboard/src/app/login/page.tsx b/dashboard/src/app/login/page.tsx new file mode 100644 index 0000000..d19b148 --- /dev/null +++ b/dashboard/src/app/login/page.tsx @@ -0,0 +1,81 @@ +'use client'; + +import React, { Suspense, useState } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { KeyRound } from 'lucide-react'; +import { setWriteToken } from '@/lib/clientApi'; + +function LoginForm() { + const router = useRouter(); + const searchParams = useSearchParams(); + const [token, setToken] = useState(''); + const [error, setError] = useState(null); + const [checking, setChecking] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setChecking(true); + try { + const res = await fetch('/api/auth/check', { + headers: { Authorization: `Bearer ${token}` }, + }); + if (res.status === 204) { + setWriteToken(token); + router.push(searchParams.get('from') ?? '/'); + return; + } + if (res.status === 503) { + setError('Escrita desabilitada no servidor (DASHBOARD_WRITE_TOKEN não configurado).'); + } else { + setError('Token inválido.'); + } + } catch { + setError('Falha ao validar o token.'); + } finally { + setChecking(false); + } + }; + + return ( +
+
+
+ +

Acesso de escrita

+
+

+ Informe o token de escrita do dashboard para criar projetos, editar + tasks e controlar execuções. +

+ setToken(e.target.value)} + placeholder="Token" + autoFocus + className="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:border-cyan-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100" + /> + {error &&

{error}

} + +
+
+ ); +} + +export default function LoginPage() { + return ( + + + + ); +} diff --git a/dashboard/src/app/not-found.tsx b/dashboard/src/app/not-found.tsx new file mode 100644 index 0000000..8b98e2e --- /dev/null +++ b/dashboard/src/app/not-found.tsx @@ -0,0 +1,12 @@ +import Link from 'next/link'; + +export default function NotFound() { + return ( +
+

Página não encontrada

+ + Voltar para o início + +
+ ); +} \ No newline at end of file diff --git a/dashboard/src/app/page.tsx b/dashboard/src/app/page.tsx new file mode 100644 index 0000000..d2a0e41 --- /dev/null +++ b/dashboard/src/app/page.tsx @@ -0,0 +1,181 @@ +import { Suspense } from 'react'; +import { promises as fs } from 'fs'; +import { join } from 'path'; +import Link from 'next/link'; + +import { ProjectCard } from '@/components/ProjectCard'; +import AlertBanner from '@/components/AlertBanner'; +import GlobalStats from '@/components/GlobalStats'; +import BudgetCard from '@/components/BudgetCard'; +import { RefreshController } from '@/components/RefreshController'; +import { getCheckpoint } from '@/lib/data'; +import { DATA_PATH } from '@/lib/squireStatePath'; +import type { Alert, RateLimitState } from '@/lib/types'; + +interface ProjectJson { + id: string; + name: string; + description: string; + status: string; + updated_at: string; +} + +interface TaskJson { + id: string; + status: string; +} + + +async function readJson(path: string): Promise { + try { + const raw = await fs.readFile(path, 'utf-8'); + return JSON.parse(raw) as T; + } catch { + return null; + } +} + +async function getProjectsWithProgress() { + const projectsDir = join(DATA_PATH, 'projects'); + let entries: string[] = []; + try { + const dirents = await fs.readdir(projectsDir, { withFileTypes: true }); + entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name); + } catch { + return []; + } + + const results = await Promise.all( + entries.map(async (name) => { + const project = await readJson(join(projectsDir, name, 'project.json')); + if (!project) return null; + + const tasksData = await readJson<{ tasks: TaskJson[] } | TaskJson[]>( + join(projectsDir, name, 'tasks.json') + ); + const tasks: TaskJson[] = Array.isArray(tasksData) + ? tasksData + : (tasksData as { tasks: TaskJson[] })?.tasks ?? []; + + const completed = tasks.filter((t) => t.status === 'completed').length; + return { project, tasks, completedTasks: completed }; + }) + ); + + return results.filter(Boolean) as { + project: ProjectJson; + tasks: TaskJson[]; + completedTasks: number; + }[]; +} + +async function getAlerts(): Promise { + const wrapper = await readJson<{ alerts: Alert[] }>(join(DATA_PATH, 'alerts.json')); + return wrapper?.alerts ?? []; +} + +function mapStatus( + status: string +): 'active' | 'completed' | 'on-hold' | 'failed' | 'blocked' { + if (status === 'implementing' || status === 'planning') return 'active'; + if (status === 'completed') return 'completed'; + if (status === 'blocked') return 'blocked'; + if (status === 'paused') return 'on-hold'; + if (status === 'failed') return 'failed'; + return 'active'; +} + +async function getGlobalStatsData() { + const stats = await readJson(join(DATA_PATH, 'global-stats.json')); + return stats ?? null; +} + +async function getRateLimits( + projectIds: string[] +): Promise> { + const checkpoints = await Promise.all( + projectIds.map(async (id) => { + const cp = await getCheckpoint(id); + return cp ? { project_id: id, rate_limit: cp.rate_limit } : null; + }) + ); + return checkpoints.filter( + (c): c is { project_id: string; rate_limit: RateLimitState } => c !== null + ); +} + +export default async function HomePage() { + const [projectData, alerts, stats] = await Promise.all([ + getProjectsWithProgress(), + getAlerts(), + getGlobalStatsData(), + ]); + const rateLimits = await getRateLimits( + projectData.map(({ project }) => project.id) + ); + + return ( +
+ {/* Barra de alertas no topo */} + {alerts.length > 0 && } + +
0 ? 'pt-24' : ''}`}> + {/* Cabeçalho */} +
+
+

+ Squire Dashboard +

+

+ {projectData.length} projeto{projectData.length !== 1 ? 's' : ''} monitorado + {projectData.length !== 1 ? 's' : ''} +

+
+ + {/* Indicador de auto-refresh — client component */} + + + +
+ + {/* Métricas globais + budget */} +
+
+ +
+
+ +
+
+ + {/* Lista de projetos */} + {projectData.length === 0 ? ( +
+

Nenhum projeto encontrado.

+

+ Verifique se SQUIRE_DATA_PATH aponta para o + diretório correto. +

+
+ ) : ( +
+ {projectData.map(({ project, tasks, completedTasks }) => ( + + t.status === 'blocked').length} + lastUpdated={new Date(project.updated_at)} + /> + + ))} +
+ )} +
+
+ ); +} diff --git a/dashboard/src/app/projects/[id]/error.tsx b/dashboard/src/app/projects/[id]/error.tsx new file mode 100644 index 0000000..53a62de --- /dev/null +++ b/dashboard/src/app/projects/[id]/error.tsx @@ -0,0 +1,46 @@ +'use client'; + +import Link from 'next/link'; +import { AlertTriangle } from 'lucide-react'; + +export default function ProjectError({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + return ( +
+
+
+ +

+ Falha ao renderizar este projeto +

+
+

+ {error.message || 'Erro inesperado.'} +

+

+ Causa comum: arquivo de estado malformado (tasks.json, + checkpoint.json…) — confira o diretório do projeto em squire-state. +

+
+ + + ← Todos os projetos + +
+
+
+ ); +} diff --git a/dashboard/src/app/projects/[id]/page.tsx b/dashboard/src/app/projects/[id]/page.tsx new file mode 100644 index 0000000..4ee9552 --- /dev/null +++ b/dashboard/src/app/projects/[id]/page.tsx @@ -0,0 +1,169 @@ +import { notFound } from 'next/navigation'; +import Link from 'next/link'; +import { + getProject, + getTasks, + getHistory, + getCommits, + getCheckpoint, + getHomologationLog, +} from '@/lib/data'; +import TaskList from '@/components/TaskList'; +import { Timeline } from '@/components/Timeline'; +import { CommitLog } from '@/components/CommitLog'; +import { CheckpointPanel } from '@/components/CheckpointPanel'; +import ProjectSettings from '@/components/ProjectSettings'; +import PlanTasksPanel from '@/components/PlanTasksPanel'; +import RunControls from '@/components/RunControls'; +import { readSessionLock } from '@/lib/squireLock'; +import { TDDProgressBar } from '@/components/TDDProgressBar'; +import { RefreshController } from '@/components/RefreshController'; +import CommandChips from '@/components/CommandChips'; +import { isResumable, projectCommands } from '@/lib/cliHints'; +import { PROJECT_STATUS_LABELS } from '@/lib/statusMaps'; +import type { ProjectStatus } from '@/lib/types'; + +const statusColors: Record = { + planning: 'bg-gray-100 text-gray-700', + implementing: 'bg-blue-100 text-blue-700', + reviewing: 'bg-yellow-100 text-yellow-700', + blocked: 'bg-red-100 text-red-700', + completed: 'bg-green-100 text-green-700', +}; + + + +export default async function ProjectPage({ params }: { params: { id: string } }) { + const { id } = params; + + const [project, tasks, history, commits, checkpoint, lock, homologationLog] = + await Promise.all([ + getProject(id), + getTasks(id), + getHistory(id), + getCommits(id), + getCheckpoint(id), + readSessionLock(), + getHomologationLog(id), + ]); + + if (!project) notFound(); + + const status = project.status as ProjectStatus; + const currentTask = + checkpoint && checkpoint.cursor.current_task_id + ? tasks.find((t) => t.id === checkpoint.cursor.current_task_id) ?? null + : null; + const showTDD = + currentTask !== null && + checkpoint !== null && + currentTask.tdd === true && + (['red_phase', 'llm_execution', 'testing', 'homologation'] as const).includes( + checkpoint.cursor.step as 'red_phase' | 'llm_execution' | 'testing' | 'homologation' + ); + + const cliHints = projectCommands({ project, tasks, lock, checkpoint }); + const resumableTaskId = isResumable(checkpoint, project, lock) + ? checkpoint!.cursor.current_task_id + : null; + + return ( +
+
+ + {/* Header */} +
+
+ + ← Todos os projetos + +

{project.name}

+

{project.description}

+
+
+ + + {project.coding_backend && ( + + ⚙ {project.coding_backend} + + )} + + {PROJECT_STATUS_LABELS[status] ?? status} + +
+
+ + {/* Comandos de desobstrução (contextuais ao estado) */} + {cliHints.length > 0 && ( +
+

+ Comandos +

+ +
+ )} + + {/* Live TDD progress (only when a task is actively running) */} + {showTDD && currentTask && checkpoint && ( +
+

+ Progresso TDD — {currentTask.title} +

+ +
+ )} + + {/* Checkpoint panel (full width) */} + {checkpoint && } + + {/* Main content: tasks + timeline */} +
+
+

+ Tasks ({tasks.length}) +

+ +
+
+

+ Histórico +

+ +
+
+ + {/* Commit log */} +
+

Commits

+ +
+ + {/* Planejamento com Claude */} + + + {/* Configurações */} + + +
+
+ ); +} diff --git a/dashboard/src/app/projects/new/page.tsx b/dashboard/src/app/projects/new/page.tsx new file mode 100644 index 0000000..ceb8e61 --- /dev/null +++ b/dashboard/src/app/projects/new/page.tsx @@ -0,0 +1,24 @@ +import Link from 'next/link'; +import NewProjectForm from '@/components/NewProjectForm'; + +export const metadata = { title: 'Novo projeto — Squire Dashboard' }; + +export default function NewProjectPage() { + return ( +
+
+ + ← Projetos + +

+ Novo projeto +

+

+ O projeto é criado pelo agente host (squire agent) — estado em + squire-state + repositório git. +

+
+ +
+ ); +} diff --git a/dashboard/src/components/AlertBanner.test.tsx b/dashboard/src/components/AlertBanner.test.tsx new file mode 100644 index 0000000..10676a5 --- /dev/null +++ b/dashboard/src/components/AlertBanner.test.tsx @@ -0,0 +1,143 @@ +import { render, screen, fireEvent, act, waitFor } from '@testing-library/react'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import AlertBanner from './AlertBanner'; +import type { Alert } from '@/lib/types'; + +// The dismiss button now hits POST /api/alerts/ack before falling back to +// localStorage; stub fetch so each test resolves deterministically. +beforeEach(() => { + vi.stubGlobal( + 'fetch', + vi.fn(async () => new Response(JSON.stringify({ updated: 1 }), { status: 200 })) + ); +}); + +const makeAlert = (overrides?: Partial): Alert => ({ + project_id: 'projeto-x', + severity: 'warning', + type: 'failed_homologation', + task_id: 'task-001', + message: 'Falhou em 5 homologações', + created_at: '2026-03-29T10:00:00Z', + acknowledged: false, + ...overrides, +}); + +beforeEach(() => { + localStorage.clear(); + vi.restoreAllMocks(); +}); + +describe('AlertBanner', () => { + it('retorna null antes de montar (evita flash de hidratação SSR)', async () => { + const { container } = render(); + await act(async () => {}); + // Após montar, o alerta deve aparecer + expect(container.innerHTML).not.toBe(''); + }); + + it('retorna null quando não há alertas', async () => { + const { container } = render(); + await act(async () => {}); + expect(container.innerHTML).toBe(''); + }); + + it('retorna null quando todos os alertas estão acknowledged', async () => { + const { container } = render( + + ); + await act(async () => {}); + expect(container.innerHTML).toBe(''); + }); + + it('renderiza project_id e task_id corretamente', async () => { + render(); + await act(async () => {}); + expect(screen.getByText(/projeto-x/)).toBeInTheDocument(); + expect(screen.getByText(/task-001/)).toBeInTheDocument(); + }); + + it('renderiza sem task_id quando task_id é null', async () => { + render(); + await act(async () => {}); + expect(screen.getByText('projeto-x')).toBeInTheDocument(); + }); + + it('não exibe "Invalid Date" no timestamp', async () => { + render(); + await act(async () => {}); + expect(screen.queryByText(/Invalid Date/)).not.toBeInTheDocument(); + }); + + it('botão dismiss remove o alerta da tela', async () => { + render(); + await act(async () => {}); + + expect(screen.getByText('Falhou em 5 homologações')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: /descartar alerta/i })); + + await waitFor(() => + expect(screen.queryByText('Falhou em 5 homologações')).not.toBeInTheDocument() + ); + }); + + it('dismiss persiste no localStorage com chave composta', async () => { + render(); + await act(async () => {}); + + fireEvent.click(screen.getByRole('button', { name: /descartar alerta/i })); + + await waitFor(() => { + const stored = JSON.parse( + localStorage.getItem('dismissed_alerts') ?? '[]' + ) as string[]; + expect(stored).toContain('projeto-x::task-001::2026-03-29T10:00:00Z'); + }); + }); + + it('alerta já dispensado (localStorage pré-populado) não aparece', async () => { + localStorage.setItem( + 'dismissed_alerts', + JSON.stringify(['projeto-x::task-001::2026-03-29T10:00:00Z']) + ); + + const { container } = render(); + await act(async () => {}); + expect(container.innerHTML).toBe(''); + }); + + it('renderiza múltiplos alertas com dismiss individual', async () => { + const alerts: Alert[] = [ + makeAlert({ task_id: 'task-001', message: 'Mensagem 1', created_at: '2026-03-29T10:00:00Z' }), + makeAlert({ task_id: 'task-002', message: 'Mensagem 2', severity: 'critical', created_at: '2026-03-29T11:00:00Z' }), + ]; + + render(); + await act(async () => {}); + + expect(screen.getByText('Mensagem 1')).toBeInTheDocument(); + expect(screen.getByText('Mensagem 2')).toBeInTheDocument(); + + const buttons = screen.getAllByRole('button', { name: /descartar alerta/i }); + fireEvent.click(buttons[0]); + + await waitFor(() => + expect(screen.queryByText('Mensagem 1')).not.toBeInTheDocument() + ); + expect(screen.getByText('Mensagem 2')).toBeInTheDocument(); + }); + + it('alerta critical tem fundo vermelho', async () => { + render(); + await act(async () => {}); + const alertEl = screen.getByRole('alert'); + expect(alertEl).toHaveStyle({ backgroundColor: '#ef4444' }); + }); + + it('alerta warning tem fundo âmbar', async () => { + render(); + await act(async () => {}); + const alertEl = screen.getByRole('alert'); + expect(alertEl).toHaveStyle({ backgroundColor: '#f59e0b' }); + }); +}); diff --git a/dashboard/src/components/AlertBanner.tsx b/dashboard/src/components/AlertBanner.tsx new file mode 100644 index 0000000..ef5eaf3 --- /dev/null +++ b/dashboard/src/components/AlertBanner.tsx @@ -0,0 +1,218 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { authedFetch } from '@/lib/clientApi'; +import { Alert } from '@/lib/types'; + +interface AlertBannerProps { + alerts: Alert[]; +} + +const STORAGE_KEY = 'dismissed_alerts'; + +const alertKey = (alert: Alert) => + `${alert.project_id}::${alert.task_id ?? ''}::${alert.created_at}`; + +async function ackAlert(alert: Alert, dismiss: boolean) { + const res = await authedFetch('/api/alerts/ack', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + project_id: alert.project_id, + task_id: alert.task_id, + created_at: alert.created_at, + dismiss, + }), + }); + if (!res.ok) { + const err = await res.json().catch(() => ({ error: 'unknown' })); + throw new Error(err.error ?? 'request_failed'); + } +} + +const AlertBanner: React.FC = ({ alerts }) => { + const router = useRouter(); + const [hasMounted, setHasMounted] = useState(false); + const [dismissedKeys, setDismissedKeys] = useState>(new Set()); + const [pending, setPending] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + setDismissedKeys(new Set(JSON.parse(stored) as string[])); + } + } catch { + // localStorage unavailable + } + setHasMounted(true); + }, []); + + const markDismissedLocally = (key: string) => { + setDismissedKeys((prev) => { + const next = new Set(prev); + next.add(key); + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(next))); + } catch { + // ignore + } + return next; + }); + }; + + const handleAck = async (alert: Alert) => { + const key = alertKey(alert); + setPending(key); + setError(null); + try { + await ackAlert(alert, false); + markDismissedLocally(key); + router.refresh(); + } catch (err) { + setError((err as Error).message); + } finally { + setPending(null); + } + }; + + const handleDismiss = async (alert: Alert) => { + const key = alertKey(alert); + setPending(key); + setError(null); + try { + await ackAlert(alert, true); + markDismissedLocally(key); + router.refresh(); + } catch (err) { + // Fall back to local dismiss so UI doesn't get stuck + markDismissedLocally(key); + setError((err as Error).message); + } finally { + setPending(null); + } + }; + + if (!hasMounted) return null; + + const activeAlerts = alerts.filter( + (alert) => !alert.acknowledged && !dismissedKeys.has(alertKey(alert)) + ); + + if (activeAlerts.length === 0) { + return null; + } + + const getBannerStyle = (severity: string) => { + if (severity === 'critical') { + return { + backgroundColor: '#ef4444', + color: '#ffffff', + borderColor: '#b91c1c', + }; + } + return { + backgroundColor: '#f59e0b', + color: '#1f2937', + borderColor: '#d97706', + }; + }; + + const getIcon = () => ( + + ); + + return ( +
+
+
+ {activeAlerts.map((alert) => { + const key = alertKey(alert); + const style = getBannerStyle(alert.severity); + const isBusy = pending === key; + return ( +
+
{getIcon()}
+
+
+
+ {alert.project_id} + {alert.task_id ? ` — ${alert.task_id}` : ''} +
+
+ {new Date(alert.created_at).toLocaleString()} +
+
+

{alert.message}

+
+
+ + +
+
+ ); + })} +
+ {error && ( +
+ Erro: {error} +
+ )} +
+
+ ); +}; + +export default AlertBanner; diff --git a/dashboard/src/components/BlockedTaskPanel.test.tsx b/dashboard/src/components/BlockedTaskPanel.test.tsx new file mode 100644 index 0000000..df39e97 --- /dev/null +++ b/dashboard/src/components/BlockedTaskPanel.test.tsx @@ -0,0 +1,116 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import BlockedTaskPanel from './BlockedTaskPanel'; +import { newTask } from '@/lib/taskDefaults'; +import type { HomologationLogEntry, Task } from '@/lib/types'; + +const blockedTask = (): Task => ({ + ...newTask({ id: 'task-009', title: 'Travada' }), + status: 'blocked', + rejection_summaries: ['resumo antigo A', 'resumo antigo B'], +}); + +const entry = (over: Partial = {}): HomologationLogEntry => ({ + timestamp: '2026-06-11T12:00:00Z', + task_id: 'task-009', + attempt: 4, + approved: false, + summary: 'save() retorna tipo errado', + feedback: 'explicação longa do problema', + fix_suggestion: 'retorne a tupla esperada', + suggestions: [], + source: 'session', + cost_usd: 0.04, + model: 'claude-x', + ...over, +}); + +describe('BlockedTaskPanel', () => { + beforeEach(() => { + vi.stubGlobal('fetch', vi.fn()); + vi.spyOn(window, 'confirm').mockReturnValue(true); + }); + afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + it('mostra vereditos do log (mais recente primeiro) com fix_suggestion expansível', () => { + render( + + ); + const rounds = screen.getAllByText(/Rodada \d/); + expect(rounds[0]).toHaveTextContent('Rodada 4'); + expect(screen.queryByText('retorne a tupla esperada')).not.toBeInTheDocument(); + fireEvent.click(screen.getByText('save() retorna tipo errado')); + expect(screen.getByText('retorne a tupla esperada')).toBeInTheDocument(); + expect(screen.getByText('explicação longa do problema')).toBeInTheDocument(); + }); + + it('faz fallback para rejection_summaries sem log', () => { + render( + + ); + expect(screen.getByText('resumo antigo A')).toBeInTheDocument(); + expect(screen.getByText(/vereditos completos indisponíveis/)).toBeInTheDocument(); + }); + + it('marca vereditos vindos do fix', () => { + render( + + ); + expect(screen.getByText('via fix')).toBeInTheDocument(); + }); + + it('botão Corrigir enfileira fix_task', async () => { + (fetch as ReturnType) + .mockResolvedValueOnce({ + ok: true, + status: 202, + json: async () => ({ id: '12345678-1234-1234-1234-123456789abc' }), + }) + // polls subsequentes + .mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ status: 'pending', command: null, result: null }), + }); + + render( + + ); + fireEvent.click(screen.getByRole('button', { name: /corrigir com claude/i })); + + await waitFor(() => { + expect(fetch).toHaveBeenCalledWith( + '/api/commands', + expect.objectContaining({ method: 'POST' }) + ); + }); + const body = JSON.parse( + (fetch as ReturnType).mock.calls[0][1].body + ); + expect(body).toMatchObject({ + type: 'fix_task', + project_id: 'proj', + args: { task_id: 'task-009' }, + }); + }); + + it('cancela quando o confirm é recusado', () => { + vi.spyOn(window, 'confirm').mockReturnValue(false); + render( + + ); + fireEvent.click(screen.getByRole('button', { name: /corrigir com claude/i })); + expect(fetch).not.toHaveBeenCalled(); + }); +}); diff --git a/dashboard/src/components/BlockedTaskPanel.tsx b/dashboard/src/components/BlockedTaskPanel.tsx new file mode 100644 index 0000000..9777676 --- /dev/null +++ b/dashboard/src/components/BlockedTaskPanel.tsx @@ -0,0 +1,200 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { ChevronDown, ChevronRight, Wrench } from 'lucide-react'; +import { enqueueCommand } from '@/lib/clientApi'; +import { useCommandPoll } from '@/hooks/useCommandPoll'; +import { taskCommands } from '@/lib/cliHints'; +import CommandChips from './CommandChips'; +import type { HomologationLogEntry, Task } from '@/lib/types'; + +interface BlockedTaskPanelProps { + task: Task; + projectId: string; + logEntries: HomologationLogEntry[]; +} + +function VerdictCard({ entry }: { entry: HomologationLogEntry }) { + const [expanded, setExpanded] = useState(false); + return ( +
+ + + {expanded && ( +
+ {entry.fix_suggestion && ( +
+

+ Como corrigir +

+

+ {entry.fix_suggestion} +

+
+ )} + {entry.feedback && ( +
+

+ Feedback completo +

+

{entry.feedback}

+
+ )} +
+ )} +
+ ); +} + +export default function BlockedTaskPanel({ + task, + projectId, + logEntries, +}: BlockedTaskPanelProps) { + const router = useRouter(); + const [commandId, setCommandId] = useState(null); + const [error, setError] = useState(null); + const poll = useCommandPoll(commandId); + const busy = poll.phase === 'pending' || poll.phase === 'running'; + + const taskVerdicts = logEntries + .filter((e) => e.task_id === task.id && !e.approved) + .reverse(); // mais recente primeiro + + useEffect(() => { + if (poll.phase === 'done') { + setCommandId(null); + router.refresh(); // task agora completed + } + if (poll.phase === 'failed') { + // exit 6 = rodou mas foi rejeitada — refresh mostra o novo veredito + setError( + poll.result?.stderr_tail || + poll.result?.stdout_tail?.split('\n').slice(-3).join('\n') || + poll.error || + 'fix falhou' + ); + setCommandId(null); + router.refresh(); + } + if (poll.phase === 'timeout') { + setError( + 'Sem resposta dentro do tempo — o fix pode ainda estar rodando no host (squire agent).' + ); + setCommandId(null); + } + }, [poll.phase, poll.error, poll.result, router]); + + const startFix = async () => { + if ( + !window.confirm( + 'Claude vai implementar a correção diretamente, rodar os testes e fazer 1 rodada de homologação (~5–8 min, ~$0.10–0.30). Continuar?' + ) + ) { + return; + } + setError(null); + try { + const id = await enqueueCommand('fix_task', projectId, { + task_id: task.id, + }); + setCommandId(id); + } catch (e) { + setError((e as Error).message); + } + }; + + return ( +
+
+ + Task bloqueada — histórico de rejeições + + +
+ + + + {busy && ( +

+ {poll.phase === 'pending' + ? 'Aguardando o agente…' + : 'Claude corrigindo — implementação + testes + homologação (~5–8 min).'} +

+ )} + {error && ( +
+          {error}
+        
+ )} + + {taskVerdicts.length > 0 ? ( +
+ {taskVerdicts.map((entry, i) => ( + + ))} +
+ ) : task.rejection_summaries.length > 0 ? ( +
+

+ (vereditos completos indisponíveis para rodadas antigas — resumos:) +

+ {[...task.rejection_summaries].reverse().map((summary, i) => ( +
+ {summary} +
+ ))} +
+ ) : ( +

Sem histórico de rejeições registrado.

+ )} +
+ ); +} diff --git a/dashboard/src/components/BudgetCard.tsx b/dashboard/src/components/BudgetCard.tsx new file mode 100644 index 0000000..42dfb63 --- /dev/null +++ b/dashboard/src/components/BudgetCard.tsx @@ -0,0 +1,146 @@ +import type { GlobalStats, RateLimitState } from "@/lib/types"; + +interface BudgetCardProps { + stats: GlobalStats | null; + rateLimits: Array<{ project_id: string; rate_limit: RateLimitState }>; +} + +function pickActiveBudget( + rateLimits: BudgetCardProps["rateLimits"] +): { project_id: string; rate_limit: RateLimitState } | null { + const withCap = rateLimits.filter((r) => r.rate_limit.max_daily_usd > 0); + if (withCap.length === 0) return null; + return withCap.reduce((acc, cur) => + cur.rate_limit.daily_cost_usd > acc.rate_limit.daily_cost_usd ? cur : acc + ); +} + +function formatUsd(n: number): string { + if (n < 1) return `$${n.toFixed(2)}`; + if (n < 100) return `$${n.toFixed(2)}`; + return `$${Math.round(n)}`; +} + +export default function BudgetCard({ stats, rateLimits }: BudgetCardProps) { + const dailyCost = stats?.cost_estimate_usd ?? 0; + const active = pickActiveBudget(rateLimits); + const cap = active?.rate_limit.max_daily_usd ?? 0; + const pct = cap > 0 ? Math.min(100, (dailyCost / cap) * 100) : 0; + + const breakdown = Object.entries(stats?.cost_by_model ?? {}) + .filter(([, v]) => v > 0) + .sort((a, b) => b[1] - a[1]); + + let ringColor = "stroke-emerald-500"; + let badge = "text-emerald-700 bg-emerald-50"; + let badgeLabel = "OK"; + if (pct >= 100) { + ringColor = "stroke-red-600"; + badge = "text-red-700 bg-red-50"; + badgeLabel = "Estourado"; + } else if (pct >= 75) { + ringColor = "stroke-amber-500"; + badge = "text-amber-700 bg-amber-50"; + badgeLabel = "Atenção"; + } else if (cap === 0) { + ringColor = "stroke-gray-300"; + badge = "text-gray-600 bg-gray-100"; + badgeLabel = "Sem cap"; + } + + // SVG ring geometry + const radius = 36; + const circumference = 2 * Math.PI * radius; + const dash = (Math.max(0, Math.min(100, pct)) / 100) * circumference; + + return ( +
+
+
+ + + + +
+ + {formatUsd(dailyCost)} + + {cap > 0 ? ( + + / {formatUsd(cap)} + + ) : ( + sem cap + )} +
+
+ +
+
+

+ Custo do dia +

+ + {badgeLabel} + +
+ + {breakdown.length > 0 ? ( +
    + {breakdown.slice(0, 4).map(([model, value]) => { + const total = + breakdown.reduce((acc, [, v]) => acc + v, 0) || 1; + const sharePct = Math.round((value / total) * 100); + return ( +
  • + + {model} + + + + + {formatUsd(value)} +
  • + ); + })} +
+ ) : ( +

+ Sem breakdown por modelo ainda. +

+ )} + + {active && ( +

+ Cap visível: {active.project_id} +

+ )} +
+
+
+ ); +} diff --git a/dashboard/src/components/CheckpointPanel.test.tsx b/dashboard/src/components/CheckpointPanel.test.tsx new file mode 100644 index 0000000..bf2ad90 --- /dev/null +++ b/dashboard/src/components/CheckpointPanel.test.tsx @@ -0,0 +1,152 @@ +import { render, screen } from '@testing-library/react'; +import { CheckpointPanel } from './CheckpointPanel'; +import type { Checkpoint } from '@/lib/types'; + +const createCheckpoint = (overrides?: Partial): Checkpoint => ({ + version: 1, + session_id: 'sess-abc123', + phase: 'implementing', + started_at: '2026-03-29T10:00:00Z', + last_heartbeat: '2026-03-29T10:30:00Z', + cursor: { + current_task_id: 'task-456', + current_subtask_id: 'sub-789', + step: 'llm_execution', + attempt: 2, + homologation_attempt: 1, + ...overrides?.cursor, + }, + llm_context: { + last_instruction: + 'Implementar função de validação de email com regex e testar casos edge case', + files_touched: ['src/utils/email.ts', 'src/types/auth.ts', 'src/validation.ts'], + last_error: null, + tests_passing: 5, + tests_failing: 0, + test_summary: 'Todos os testes passaram', + ...overrides?.llm_context, + }, + rate_limit: { + claude_code_calls_this_window: 10, + window_started_at: '2026-03-29T10:00:00Z', + window_duration_minutes: 60, + max_calls_per_window: 100, + max_daily_usd: 0, + daily_cost_usd: 0, + daily_cost_date: '', + }, + recovery: { + can_resume: true, + resume_action: 'Continuar execução normal', + blocked_reason: null, + escalation_needed: false, + ...overrides?.recovery, + }, + ...overrides, +}); + +describe('CheckpointPanel', () => { + it('retorna null quando checkpoint=null', () => { + const { container } = render(); + expect(container.innerHTML).toBe(''); + }); + + it('renderiza task ID e step badge corretamente', () => { + const checkpoint = createCheckpoint({ + cursor: { + current_task_id: 'task-xyz', + current_subtask_id: null, + step: 'red_phase', + attempt: 3, + homologation_attempt: 2, + }, + }); + + render(); + + expect(screen.getByText('Tarefa:')).toBeInTheDocument(); + expect(screen.getByText('task-xyz')).toBeInTheDocument(); + // label PT do statusMaps; enum cru fica no title + expect(screen.getByText('teste (RED)')).toBeInTheDocument(); + expect(screen.getByTitle('red_phase')).toBeInTheDocument(); + expect(screen.getByText('Tentativa #3')).toBeInTheDocument(); + expect(screen.getByText('Homologação #2')).toBeInTheDocument(); + }); + + it('exibe Nenhuma quando current_task_id é null', () => { + const checkpoint = createCheckpoint({ + cursor: { + current_task_id: null, + current_subtask_id: null, + step: 'planning', + attempt: 1, + homologation_attempt: 0, + }, + }); + + render(); + expect(screen.getByText('Nenhuma')).toBeInTheDocument(); + }); + + it('mostra seção de recovery quando escalation_needed=true ou can_resume=false', () => { + const checkpointWithEscalation = createCheckpoint({ + recovery: { + can_resume: true, + resume_action: 'Reiniciar sessão', + blocked_reason: 'Timeout na execução', + escalation_needed: true, + }, + }); + + render(); + expect(screen.getByText('Escalação necessária')).toBeInTheDocument(); + expect(screen.getByText('Bloqueado: Timeout na execução')).toBeInTheDocument(); + + const checkpointNoResume = createCheckpoint({ + recovery: { + can_resume: false, + resume_action: 'Reiniciar sessão', + blocked_reason: 'Dependência não resolvida', + escalation_needed: false, + }, + }); + + render(); + expect(screen.getByText('Bloqueado: Dependência não resolvida')).toBeInTheDocument(); + }); + + it('oculta seção de recovery quando ambos ok (escalation_needed=false e can_resume=true)', () => { + const checkpoint = createCheckpoint({ + recovery: { + can_resume: true, + resume_action: 'Continuar', + blocked_reason: null, + escalation_needed: false, + }, + }); + + render(); + expect(screen.queryByText('Escalação necessária')).not.toBeInTheDocument(); + expect(screen.queryByText('Bloqueado:')).not.toBeInTheDocument(); + }); + + it('trunca last_instruction em 150 chars e 2 linhas', () => { + const longInstruction = + 'Esta é uma instrução muito longa que excede 150 caracteres e deve ser truncada corretamente pelo componente CheckpointPanel para não quebrar o layout da interface do usuário'; + const checkpoint = createCheckpoint({ + llm_context: { + last_instruction: longInstruction, + files_touched: [], + last_error: null, + tests_passing: 0, + tests_failing: 0, + test_summary: '', + }, + }); + + render(); + const instructionText = screen.getByText(/Última instrução:/).nextElementSibling; + expect(instructionText?.textContent).toMatch(/...$/); + expect(instructionText?.textContent?.split('\n').length).toBeLessThanOrEqual(3); + }); +}); diff --git a/dashboard/src/components/CheckpointPanel.tsx b/dashboard/src/components/CheckpointPanel.tsx new file mode 100644 index 0000000..5c632d0 --- /dev/null +++ b/dashboard/src/components/CheckpointPanel.tsx @@ -0,0 +1,165 @@ +import { CURSOR_STEP_LABELS } from '@/lib/statusMaps'; +import type { Checkpoint, CursorStep } from '@/lib/types'; +import { RateLimitGauge } from './RateLimitGauge'; + +interface CheckpointPanelProps { + checkpoint: Checkpoint | null; +} + +const stepColors: Record = { + planning: 'bg-gray-100 text-gray-700', + red_phase: 'bg-red-100 text-red-700', + llm_execution: 'bg-blue-100 text-blue-700', + testing: 'bg-cyan-100 text-cyan-700', + homologation: 'bg-purple-100 text-purple-700', + completed: 'bg-green-100 text-green-700', +}; + +const phaseColors: Record = { + planning: 'bg-gray-100 text-gray-700', + implementing: 'bg-blue-100 text-blue-700', + reviewing: 'bg-purple-100 text-purple-700', + blocked: 'bg-red-100 text-red-700', + completed: 'bg-green-100 text-green-700', +}; + +function truncateLines(text: string, maxChars: number, maxLines: number): string { + const truncated = text.length > maxChars ? text.slice(0, maxChars) + '...' : text; + const lines = truncated.split('\n'); + if (lines.length > maxLines) { + return lines.slice(0, maxLines).join('\n') + '...'; + } + return truncated; +} + +function calculateUptime(startedAt: string, lastHeartbeat: string): string { + const start = new Date(startedAt).getTime(); + const end = new Date(lastHeartbeat).getTime(); + const diffMinutes = Math.floor((end - start) / (1000 * 60)); + return `Ativo há ${diffMinutes}min`; +} + +export function CheckpointPanel({ checkpoint }: CheckpointPanelProps) { + if (!checkpoint) { + return null; + } + + const { cursor, llm_context, phase, session_id, started_at, last_heartbeat, recovery, rate_limit } = checkpoint; + + const currentTaskId = cursor.current_task_id ?? 'Nenhuma'; + const stepColor = stepColors[cursor.step] || 'bg-gray-100 text-gray-700'; + const phaseColor = phaseColors[phase] || 'bg-gray-100 text-gray-700'; + + const instructionPreview = truncateLines(llm_context.last_instruction, 150, 2); + const testFraction = `${llm_context.tests_passing} passando / ${llm_context.tests_failing} falhando`; + const testColor = llm_context.tests_failing === 0 ? 'text-green-600' : 'text-red-600'; + const filesToShow = llm_context.files_touched.slice(0, 5); + const filesMore = llm_context.files_touched.length > 5 ? `+${llm_context.files_touched.length - 5} mais` : null; + + const showRecovery = recovery.escalation_needed || !recovery.can_resume; + + return ( +
+

Checkpoint

+ + {/* CURSOR SECTION */} +
+
+ Tarefa: + {currentTaskId} +
+
+ + {CURSOR_STEP_LABELS[cursor.step] ?? cursor.step} + + + Tentativa #{cursor.attempt} + + + Homologação #{cursor.homologation_attempt} + +
+
+ + {/* LLM CONTEXT SECTION */} +
+
+ Última instrução: +

{instructionPreview}

+
+
+ {filesToShow.map((file, idx) => ( + + {file} + + ))} + {filesMore && ( + + {filesMore} + + )} +
+
+ {testFraction} +
+
+ + {/* SESSION SECTION */} +
+
+ + {phase} + + {session_id} +
+
+ {calculateUptime(started_at, last_heartbeat)} +
+
+ +
+
+ + {/* RECOVERY SECTION */} + {showRecovery && ( +
+
+ + + +
+ {recovery.escalation_needed && ( +
+ Escalação necessária +
+ )} + {recovery.blocked_reason && ( +
+ Bloqueado: {recovery.blocked_reason} +
+ )} + {recovery.resume_action && ( +
+ Ação: {recovery.resume_action} +
+ )} +
+
+
+ )} +
+ ); +} diff --git a/dashboard/src/components/CommandChips.test.tsx b/dashboard/src/components/CommandChips.test.tsx new file mode 100644 index 0000000..ea63fcb --- /dev/null +++ b/dashboard/src/components/CommandChips.test.tsx @@ -0,0 +1,55 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import CommandChips from './CommandChips'; + +const HINTS = [ + { cmd: 'squire fix proj task-009', why: 'Claude corrige a task' }, + { cmd: 'squire unblock proj task-009', why: 'volta para pending' }, +]; + +describe('CommandChips', () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it('lista vazia não renderiza nada', () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it('renderiza comando + motivo', () => { + render(); + expect(screen.getByText('squire fix proj task-009')).toBeInTheDocument(); + expect(screen.getByText('Claude corrige a task')).toBeInTheDocument(); + }); + + it('clique copia via navigator.clipboard e mostra feedback', async () => { + const writeText = vi.fn().mockResolvedValue(undefined); + vi.stubGlobal('navigator', { clipboard: { writeText } }); + + render(); + fireEvent.click(screen.getByText('squire fix proj task-009')); + + await waitFor(() => + expect(writeText).toHaveBeenCalledWith('squire fix proj task-009') + ); + await waitFor(() => + expect(screen.getByText('copiado ✓')).toBeInTheDocument() + ); + }); + + it('sem clipboard API (HTTP de LAN) usa o fallback execCommand', async () => { + vi.stubGlobal('navigator', {}); // sem .clipboard — http não-seguro + const exec = vi.fn().mockReturnValue(true); + document.execCommand = exec as never; + + render(); + fireEvent.click(screen.getByText('squire unblock proj task-009')); + + await waitFor(() => expect(exec).toHaveBeenCalledWith('copy')); + await waitFor(() => + expect(screen.getByText('copiado ✓')).toBeInTheDocument() + ); + }); +}); diff --git a/dashboard/src/components/CommandChips.tsx b/dashboard/src/components/CommandChips.tsx new file mode 100644 index 0000000..fab73c4 --- /dev/null +++ b/dashboard/src/components/CommandChips.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { useState } from 'react'; +import { Check, Copy, Terminal } from 'lucide-react'; +import type { CliHint } from '@/lib/cliHints'; + +/** + * Copia mesmo em HTTP de LAN: navigator.clipboard exige secure context + * (o deploy real é http://192.168…), então o fallback com execCommand + * não é opcional. + */ +async function copyText(text: string): Promise { + if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) { + try { + await navigator.clipboard.writeText(text); + return true; + } catch { + // cai para o fallback + } + } + try { + const ta = document.createElement('textarea'); + ta.value = text; + ta.style.position = 'fixed'; + ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + const ok = document.execCommand('copy'); + document.body.removeChild(ta); + return ok; + } catch { + return false; + } +} + +export default function CommandChips({ hints }: { hints: CliHint[] }) { + const [copied, setCopied] = useState(null); + + if (hints.length === 0) return null; + + const handleCopy = async (cmd: string) => { + if (await copyText(cmd)) { + setCopied(cmd); + setTimeout(() => setCopied((c) => (c === cmd ? null : c)), 1500); + } + }; + + return ( +
+ {hints.map((hint) => ( +
+ + + {copied === hint.cmd ? 'copiado ✓' : hint.why} + +
+ ))} +
+ ); +} diff --git a/dashboard/src/components/CommitLog.test.tsx b/dashboard/src/components/CommitLog.test.tsx new file mode 100644 index 0000000..a478790 --- /dev/null +++ b/dashboard/src/components/CommitLog.test.tsx @@ -0,0 +1,121 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import CommitLog from './CommitLog'; +import type { CommitSummary } from '@/lib/types'; + +const makeCommit = (overrides?: Partial): CommitSummary => ({ + sha: 'abc1234def56789', + message: 'feat: commit de teste', + timestamp: '2026-03-29T10:00:00Z', + diff_summary: 'Resumo do diff', + files_changed: ['src/app/page.tsx'], + ...overrides, +}); + +const makeCommits = (count: number): CommitSummary[] => + Array.from({ length: count }, (_, i) => + makeCommit({ sha: `sha${i}`, message: `Commit ${i + 1}` }) + ); + +describe('CommitLog', () => { + it('renderiza estado vazio quando não há commits', () => { + render(); + expect(screen.getByText('Nenhum commit encontrado')).toBeInTheDocument(); + }); + + it('renderiza estado de loading quando isLoading=true', () => { + render(); + expect(screen.getByText(/carregando histórico/i)).toBeInTheDocument(); + }); + + it('renderiza todos os commits quando total ≤ pageSize', () => { + render(); + expect(screen.getByText('Commit 1')).toBeInTheDocument(); + expect(screen.getByText('Commit 5')).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /ver mais/i })).not.toBeInTheDocument(); + }); + + it('renderiza apenas pageSize commits quando total > pageSize', () => { + render(); + for (let i = 1; i <= 20; i++) { + expect(screen.getByText(`Commit ${i}`)).toBeInTheDocument(); + } + expect(screen.queryByText('Commit 21')).not.toBeInTheDocument(); + }); + + it('exibe botão "Ver mais N commits" quando há mais que pageSize', () => { + render(); + expect(screen.getByRole('button', { name: /ver mais 5 commits/i })).toBeInTheDocument(); + }); + + it('botão "Ver mais" expande lista em pageSize', () => { + render(); + expect(screen.queryByText('Commit 21')).not.toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: /ver mais/i })); + + expect(screen.getByText('Commit 21')).toBeInTheDocument(); + expect(screen.getByText('Commit 25')).toBeInTheDocument(); + }); + + it('botão desaparece quando todos os commits estão visíveis', () => { + render(); + fireEvent.click(screen.getByRole('button', { name: /ver mais/i })); + expect(screen.queryByRole('button', { name: /ver mais/i })).not.toBeInTheDocument(); + }); + + it('não renderiza heading interno duplicado', () => { + render(); + expect(screen.queryByText('Histórico de Commits')).not.toBeInTheDocument(); + }); + + it('usa pageSize=20 como default', () => { + render(); + expect(screen.getByRole('button', { name: /ver mais 1 commits/i })).toBeInTheDocument(); + }); + + it('exibe sha truncado e diff_summary', () => { + render(); + expect(screen.getByText('abcdef1')).toBeInTheDocument(); + expect(screen.getByText('Resumo visível')).toBeInTheDocument(); + }); + + it('não exibe botão de expandir quando arquivos ≤ 3', () => { + const commit = makeCommit({ files_changed: ['a.ts', 'b.ts', 'c.ts'] }); + render(); + expect(screen.queryByRole('button', { name: /arquivo/i })).not.toBeInTheDocument(); + }); + + it('exibe apenas 3 arquivos e botão "+ N arquivo(s)" quando arquivos > 3', () => { + const files = ['a.ts', 'b.ts', 'c.ts', 'd.ts', 'e.ts']; + const commit = makeCommit({ files_changed: files }); + render(); + + expect(screen.getByText('a.ts')).toBeInTheDocument(); + expect(screen.getByText('c.ts')).toBeInTheDocument(); + expect(screen.queryByText('d.ts')).not.toBeInTheDocument(); + expect(screen.getByText('+ 2 arquivo(s)')).toBeInTheDocument(); + }); + + it('expande lista de arquivos ao clicar no botão', () => { + const files = ['a.ts', 'b.ts', 'c.ts', 'd.ts', 'e.ts']; + render(); + + fireEvent.click(screen.getByText('+ 2 arquivo(s)')); + + expect(screen.getByText('d.ts')).toBeInTheDocument(); + expect(screen.getByText('e.ts')).toBeInTheDocument(); + expect(screen.getByText('Ver menos')).toBeInTheDocument(); + }); + + it('colapsa lista de arquivos ao clicar "Ver menos"', () => { + const files = ['a.ts', 'b.ts', 'c.ts', 'd.ts']; + render(); + + fireEvent.click(screen.getByText('+ 1 arquivo(s)')); + expect(screen.getByText('d.ts')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('Ver menos')); + expect(screen.queryByText('d.ts')).not.toBeInTheDocument(); + }); +}); diff --git a/dashboard/src/components/CommitLog.tsx b/dashboard/src/components/CommitLog.tsx new file mode 100644 index 0000000..55b84d3 --- /dev/null +++ b/dashboard/src/components/CommitLog.tsx @@ -0,0 +1,140 @@ +'use client'; + +import React, { useState } from 'react'; +import { CommitSummary } from '@/lib/types'; + +interface CommitLogProps { + commits: CommitSummary[]; + pageSize?: number; + isLoading?: boolean; +} + +const getRelativeTime = (dateString: string): string => { + const date = new Date(dateString); + const now = new Date(); + const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000); + + if (diffInSeconds < 60) return 'há alguns segundos'; + if (diffInSeconds < 3600) return `há ${Math.floor(diffInSeconds / 60)} minuto(s)`; + if (diffInSeconds < 86400) return `há ${Math.floor(diffInSeconds / 3600)} hora(s)`; + if (diffInSeconds < 604800) return `há ${Math.floor(diffInSeconds / 86400)} dia(s)`; + + return date.toLocaleDateString('pt-BR'); +}; + +const truncateSha = (sha: string): string => sha.substring(0, 7); + +const FILES_PREVIEW = 3; + +export const CommitLog: React.FC = ({ commits, pageSize = 20, isLoading = false }) => { + const [visibleCount, setVisibleCount] = useState(pageSize); + const [expandedFiles, setExpandedFiles] = useState>(new Set()); + + const toggleFiles = (key: string) => + setExpandedFiles((prev) => { + const next = new Set(prev); + next.has(key) ? next.delete(key) : next.add(key); + return next; + }); + + const displayedCommits = commits.slice(0, visibleCount); + const remaining = commits.length - visibleCount; + + if (isLoading) { + return ( +
+ Carregando histórico de commits... +
+ ); + } + + if (!commits || commits.length === 0) { + return ( +
+

Nenhum commit encontrado

+

O projeto ainda não possui histórico de alterações.

+
+ ); + } + + return ( +
+
+ {displayedCommits.map((commit, index) => ( +
+
+
+ + {truncateSha(commit.sha)} + + + {getRelativeTime(commit.timestamp)} + +
+
+ +

+ {commit.message} +

+ + {commit.diff_summary && ( +

+ {commit.diff_summary} +

+ )} + + {commit.files_changed && commit.files_changed.length > 0 && (() => { + const fileKey = `${commit.sha}-${index}`; + const isExpanded = expandedFiles.has(fileKey); + const hasMore = commit.files_changed.length > FILES_PREVIEW; + const visibleFiles = isExpanded + ? commit.files_changed + : commit.files_changed.slice(0, FILES_PREVIEW); + return ( +
+

+ Arquivos alterados +

+
    + {visibleFiles.map((file, fileIndex) => ( +
  • + + + {file} + +
  • + ))} +
+ {hasMore && ( + + )} +
+ ); + })()} +
+ ))} +
+ + {remaining > 0 && ( + + )} +
+ ); +}; + +export default CommitLog; diff --git a/dashboard/src/components/GlobalStats.test.tsx b/dashboard/src/components/GlobalStats.test.tsx new file mode 100644 index 0000000..2d3a99a --- /dev/null +++ b/dashboard/src/components/GlobalStats.test.tsx @@ -0,0 +1,25 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import GlobalStats from './GlobalStats'; +import type { GlobalStats as GlobalStatsType } from '@/lib/types'; + +const stats: GlobalStatsType = { + daily_claude_code_calls: 4, + daily_local_llm_calls: 10, + date: '2026-06-12', + cost_estimate_usd: 1.5, + daily_tokens: 1000, + cost_by_model: {}, + daily_calls_unknown_cost: 0, + projects_touched_today: ['p'], + tasks_completed_today: 2, + approval_first_try_rate: 50, // escala 0-100, como o squire grava +}; + +describe('GlobalStats', () => { + it('aprovação 1ª usa a escala 0-100 do arquivo (50 → "50%", não "5000%")', () => { + render(); + expect(screen.getByText('50%')).toBeInTheDocument(); + expect(screen.queryByText('5000%')).not.toBeInTheDocument(); + }); +}); diff --git a/dashboard/src/components/GlobalStats.tsx b/dashboard/src/components/GlobalStats.tsx new file mode 100644 index 0000000..ca03118 --- /dev/null +++ b/dashboard/src/components/GlobalStats.tsx @@ -0,0 +1,137 @@ +import type { GlobalStats } from '@/lib/types'; + +interface GlobalStatsProps { + stats: GlobalStats | null; +} + +function formatTokens(n: number): string { + if (n < 1000) return String(n); + if (n < 1_000_000) return `${(n / 1000).toFixed(1)}k`; + return `${(n / 1_000_000).toFixed(2)}M`; +} + +interface TileProps { + label: string; + value: React.ReactNode; + hint?: React.ReactNode; + iconBg: string; + iconText: string; + icon: React.ReactNode; +} + +function Tile({ label, value, hint, iconBg, iconText, icon }: TileProps) { + return ( +
+
{icon}
+
+

+ {label} +

+

{value}

+ {hint && ( +

{hint}

+ )} +
+
+ ); +} + +export default function GlobalStats({ stats }: GlobalStatsProps) { + const localCalls = stats?.daily_local_llm_calls ?? 0; + const claudeCalls = stats?.daily_claude_code_calls ?? 0; + const totalCalls = localCalls + claudeCalls; + const localPct = totalCalls > 0 ? Math.round((localCalls / totalCalls) * 100) : 0; + const claudePct = totalCalls > 0 ? Math.round((claudeCalls / totalCalls) * 100) : 0; + const tokens = stats?.daily_tokens ?? 0; + const unknownCalls = stats?.daily_calls_unknown_cost ?? 0; + const projectsTouched = Array.isArray(stats?.projects_touched_today) + ? stats!.projects_touched_today.length + : 0; + + return ( +
+ + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + 0 + ? 'custo real provavelmente maior' + : 'todos os backends reportaram usage' + } + iconBg={unknownCalls > 0 ? 'bg-amber-100 dark:bg-amber-900' : 'bg-gray-100 dark:bg-gray-700'} + iconText={unknownCalls > 0 ? 'text-amber-700 dark:text-amber-300' : 'text-gray-500 dark:text-gray-400'} + icon={ + + + + } + /> +
+ ); +} diff --git a/dashboard/src/components/HealthStrip.test.ts b/dashboard/src/components/HealthStrip.test.ts new file mode 100644 index 0000000..f7b2bd0 --- /dev/null +++ b/dashboard/src/components/HealthStrip.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; +import { sessionStatus } from './HealthStrip'; + +describe('sessionStatus', () => { + it('sem lock: "Squire ocioso / nenhuma sessão ativa" (não "Sem sessão sem lock")', () => { + const s = sessionStatus({ + held: false, + holder: null, + projectId: null, + acquiredAt: null, + }); + expect(s.label).toBe('Squire ocioso'); + expect(s.detail).toBe('nenhuma sessão ativa'); + expect(`${s.label} ${s.detail}`).not.toContain('Sem sessão sem lock'); + }); + + it('lock expirado: ocioso com detalhe explícito', () => { + const s = sessionStatus({ + held: false, + holder: 'sess-x', + projectId: 'proj', + acquiredAt: '2026-06-11T00:00:00Z', + }); + expect(s.label).toBe('Squire ocioso'); + expect(s.detail).toBe('lock expirado'); + }); + + it('ativo: mostra o projeto rodando (não o sess-id)', () => { + const s = sessionStatus({ + held: true, + holder: 'sess-20260612-abc', + projectId: 'meu-app', + acquiredAt: '2026-06-12T00:00:00Z', + }); + expect(s.label).toBe('Squire ativo'); + expect(s.detail).toBe('meu-app'); + }); + + it('ativo sem project_id (lock antigo): cai para o holder', () => { + const s = sessionStatus({ + held: true, + holder: 'sess-20260612-abc', + projectId: null, + acquiredAt: '2026-06-12T00:00:00Z', + }); + expect(s.detail).toBe('sess-20260612-abc'); + }); +}); diff --git a/dashboard/src/components/HealthStrip.tsx b/dashboard/src/components/HealthStrip.tsx new file mode 100644 index 0000000..d4237ec --- /dev/null +++ b/dashboard/src/components/HealthStrip.tsx @@ -0,0 +1,110 @@ +import Link from 'next/link'; +import { readSessionLock, type LockStatus } from '@/lib/squireLock'; +import { getAlerts, getGlobalStats, getProjects, getCheckpoint } from '@/lib/data'; +import type { RateLimitState } from '@/lib/types'; + +export interface SessionStatusView { + dot: string; + label: string; + detail: string; +} + +/** Copy do indicador de sessão — puro, para teste unitário. */ +export function sessionStatus( + lock: Pick +): SessionStatusView { + if (lock.held) { + return { + dot: 'bg-green-500', + label: 'Squire ativo', + // o projeto rodando importa mais que o id da sessão + detail: lock.projectId ?? lock.holder ?? '?', + }; + } + if (lock.acquiredAt) { + return { dot: 'bg-gray-400', label: 'Squire ocioso', detail: 'lock expirado' }; + } + return { dot: 'bg-gray-300', label: 'Squire ocioso', detail: 'nenhuma sessão ativa' }; +} + +async function gatherBudget(): Promise<{ + spent: number; + cap: number; + pct: number; +} | null> { + const projects = await getProjects(); + if (projects.length === 0) return null; + + const rateLimits: RateLimitState[] = []; + for (const p of projects) { + const cp = await getCheckpoint(p.id); + if (cp?.rate_limit) rateLimits.push(cp.rate_limit); + } + + const stats = await getGlobalStats(); + const spent = stats?.cost_estimate_usd ?? 0; + const withCap = rateLimits.filter((r) => r.max_daily_usd > 0); + const cap = withCap.length === 0 ? 0 : Math.max(...withCap.map((r) => r.max_daily_usd)); + const pct = cap > 0 ? Math.min(100, (spent / cap) * 100) : 0; + return { spent, cap, pct }; +} + +export default async function HealthStrip() { + const lock = await readSessionLock(); + const alerts = await getAlerts(); + const activeAlerts = alerts.filter((a) => !a.acknowledged).length; + const budget = await gatherBudget(); + + const status = sessionStatus(lock); + + let budgetClass = 'text-gray-600'; + let budgetLabel = budget && budget.cap > 0 + ? `$${budget.spent.toFixed(2)} / $${budget.cap.toFixed(2)}` + : budget + ? `$${budget.spent.toFixed(2)}` + : '—'; + if (budget && budget.cap > 0) { + if (budget.pct >= 100) budgetClass = 'text-red-700 font-semibold'; + else if (budget.pct >= 75) budgetClass = 'text-amber-700 font-semibold'; + else budgetClass = 'text-emerald-700'; + } + + return ( +
+
+ + + + {status.label} + + + {status.detail} + + + +
+ + 💰 {budgetLabel} + + + {activeAlerts > 0 ? ( + + ⚠ {activeAlerts} + + ) : ( + + ⚠ 0 + + )} +
+
+
+ ); +} diff --git a/dashboard/src/components/NewProjectForm.tsx b/dashboard/src/components/NewProjectForm.tsx new file mode 100644 index 0000000..3f9995d --- /dev/null +++ b/dashboard/src/components/NewProjectForm.tsx @@ -0,0 +1,161 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { enqueueCommand } from '@/lib/clientApi'; +import { useCommandPoll } from '@/hooks/useCommandPoll'; + +const ID_RE = /^[a-z0-9][a-z0-9-]*$/; +const BACKENDS = ['opencode', 'litellm', 'crush']; +const REPO_ROOT = '/home/ai-debian/projects'; + +export default function NewProjectForm() { + const router = useRouter(); + const [id, setId] = useState(''); + const [name, setName] = useState(''); + const [repoPath, setRepoPath] = useState(''); + const [repoTouched, setRepoTouched] = useState(false); + const [stack, setStack] = useState('python'); + const [backend, setBackend] = useState('opencode'); + const [gitInit, setGitInit] = useState(true); + const [commandId, setCommandId] = useState(null); + const [submitError, setSubmitError] = useState(null); + + const poll = useCommandPoll(commandId); + + const effectiveRepo = repoTouched ? repoPath : id ? `${REPO_ROOT}/${id}` : ''; + const idValid = ID_RE.test(id); + const busy = poll.phase === 'pending' || poll.phase === 'running'; + + useEffect(() => { + if (poll.phase === 'done') { + router.push(`/projects/${id}`); + } + }, [poll.phase, id, router]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setSubmitError(null); + try { + const cmdId = await enqueueCommand('new_project', id, { + name: name || id, + repo_path: effectiveRepo, + stack, + backend, + git_init: gitInit, + }); + setCommandId(cmdId); + } catch (err) { + setSubmitError((err as Error).message); + } + }; + + return ( +
+ + + + + + +
+ + +
+ + + + {submitError &&

{submitError}

} + {busy && ( +

+ {poll.phase === 'pending' + ? 'Aguardando o agente… (se demorar, verifique `squire agent` na VM)' + : 'Criando projeto…'} +

+ )} + {poll.phase === 'failed' && ( +
+

Falhou: {poll.error}

+ {poll.result?.stderr_tail && ( +
{poll.result.stderr_tail}
+ )} +
+ )} + {poll.phase === 'timeout' && ( +

+ Sem resposta do agente — confira se `squire agent` está rodando na VM. +

+ )} + + +
+ ); +} diff --git a/dashboard/src/components/PlanTasksPanel.tsx b/dashboard/src/components/PlanTasksPanel.tsx new file mode 100644 index 0000000..f0b2548 --- /dev/null +++ b/dashboard/src/components/PlanTasksPanel.tsx @@ -0,0 +1,131 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { Sparkles } from 'lucide-react'; +import { enqueueCommand } from '@/lib/clientApi'; +import { useCommandPoll } from '@/hooks/useCommandPoll'; + +interface PlanTasksPanelProps { + projectId: string; + initialDescription: string; +} + +export default function PlanTasksPanel({ + projectId, + initialDescription, +}: PlanTasksPanelProps) { + const router = useRouter(); + const [open, setOpen] = useState(false); + const [description, setDescription] = useState(initialDescription); + const [mode, setMode] = useState<'append' | 'replace'>('append'); + const [commandId, setCommandId] = useState(null); + const [error, setError] = useState(null); + const [doneMsg, setDoneMsg] = useState(null); + + const poll = useCommandPoll(commandId); + const busy = poll.phase === 'pending' || poll.phase === 'running'; + + useEffect(() => { + if (poll.phase === 'done') { + setDoneMsg('Tasks planejadas — lista atualizada.'); + setCommandId(null); + router.refresh(); + } + if (poll.phase === 'failed') { + setError(poll.result?.stderr_tail || poll.error || 'falhou'); + setCommandId(null); + } + if (poll.phase === 'timeout') { + setError('Sem resposta do agente — `squire agent` está rodando na VM?'); + setCommandId(null); + } + }, [poll.phase, poll.error, poll.result, router]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setDoneMsg(null); + try { + const id = await enqueueCommand('plan_tasks', projectId, { + description, + mode, + }); + setCommandId(id); + } catch (err) { + setError((err as Error).message); + } + }; + + return ( +
+ + + {open && ( +
+