diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c345aa4..11162f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: audit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Install uv uses: astral-sh/setup-uv@v8.3.2 - name: Export locked runtime dependencies @@ -30,7 +30,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: fetch-depth: 0 - name: Install uv @@ -43,7 +43,7 @@ jobs: run: uv run pytest - name: Upload coverage report if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: coverage-report path: coverage.xml @@ -52,7 +52,7 @@ jobs: needs: test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: fetch-depth: 0 - name: Download coverage report diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1e69d1e..9fdf4ed 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -20,13 +20,13 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Install uv uses: astral-sh/setup-uv@v8.3.2 - name: Build the docs site run: uv run --group docs mkdocs build --strict - name: Upload the site artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v5 with: path: site @@ -42,4 +42,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 9356c63..6e05736 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -53,7 +53,7 @@ jobs: with: python-version: "3.12" - name: Install uv - uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8 + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8 - name: Build sdist + wheel run: uv build --out-dir dist - name: Publish to PyPI (OIDC trusted publishing) diff --git a/Dockerfile b/Dockerfile index 496b732..45abb3f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,4 +19,4 @@ EXPOSE 8000 # Apply migrations, then serve with gunicorn. Provide DATABASE_URL and # ACES_WORKBENCH_SECRET_KEY via the environment. -CMD ["sh", "-c", "aces-workbench migrate --noinput && gunicorn aces_scenario_workbench.wsgi:application --bind 0.0.0.0:8000 --workers 3"] +CMD ["sh", "-c", "aces-workbench migrate --noinput && gunicorn aces_scenario_workbench.wsgi:application --bind 0.0.0.0:${PORT:-8000} --workers ${WEB_CONCURRENCY:-3}"] diff --git a/README.md b/README.md index 51e1c9f..2ea8a63 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ aces-workbench serve # http://127.0.0.1:8000 Before the first PyPI release, install from a source checkout with `pip install .`. See [`docs/setup.md`](docs/setup.md) for the full setup -walkthrough (projects, invitations, email, password reset). +walkthrough (scenarios, invitations, email, password reset). By default the workbench uses a local SQLite database (`db.sqlite3` in the current directory). Point it at PostgreSQL by setting `DATABASE_URL`: @@ -34,7 +34,7 @@ export DATABASE_URL=postgres://user:pass@localhost:5432/workbench ``` The Django admin defaults to `/control/` and is available to administrators for -creating projects and managing membership. Set `ACES_WORKBENCH_ADMIN_PATH` to +creating scenarios and managing membership. Set `ACES_WORKBENCH_ADMIN_PATH` to use a deployment-specific admin path. ## Documentation @@ -90,14 +90,27 @@ restricted to administrators. aces-workbench serve [--host H] [--port P] # run the app (applies migrations first) aces-workbench migrate # apply database migrations aces-workbench createadmin # create an administrator account -aces-workbench import --project # import a pack's ATLAS projection +aces-workbench import --scenario # import a pack's ATLAS projection +aces-workbench sync [--slug ] # create/update a scenario and import it aces-workbench doctor # report configuration readiness aces-workbench manage [...] # any Django management command ``` -Administrators create projects and invite members from the Django admin +Administrators create scenarios and invite members from the Django admin (`/control/` by default); import a scenario pack's ATLAS technique projection with -`aces-workbench import --project `. +`aces-workbench import --scenario `. + +For local dogfooding against an actively developed pack, use `sync`. It creates +the scenario row when needed, updates explicit scenario metadata, imports the +current projection as a digest-addressed immutable revision, and does not write +back into the pack. When challenge, flag placement, objective, telemetry, and +runtime challenge contracts are present, `sync` also imports implemented +challenges and their evidence requirements into the revision: + +```bash +aces-workbench sync --slug --name "Display Name" \ + --grant-user you@example.com --role administrator +``` ## Development @@ -109,6 +122,15 @@ uv run ruff format . # format make hooks # activate commit-time hooks (once per clone) ``` +The authenticated workspace shell is a Vite/React SPA. Rebuild the Django-served +assets after frontend changes: + +```bash +cd frontend +npm install +npm run build +``` + See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the developer workflow. ## Deployment diff --git a/docs/access-control.md b/docs/access-control.md index 903ec80..8326288 100644 --- a/docs/access-control.md +++ b/docs/access-control.md @@ -18,7 +18,7 @@ In the Django admin (`/control/` by default, unless `ACES_WORKBENCH_ADMIN_PATH` is set): 1. Go to **Invitations → Add**. -2. Enter the person's **email**, the **project**, and their **role**. +2. Enter the person's **email**, the **scenario**, and their **role**. 3. Save. An email with an accept link is sent automatically. The accept link: @@ -34,9 +34,9 @@ again. (With the default console email backend the message prints to the server console instead of being delivered — configure SMTP to send for real; see [setup](setup.md).) -## Projects, membership, and roles +## Scenarios, membership, and roles -Access is scoped per project: a signed-in user only sees and acts on projects +Access is scoped per scenario: a signed-in user only sees and acts on scenarios they are a member of. This is enforced on the server — hidden navigation is never treated as a substitute for authorization. @@ -49,9 +49,9 @@ Each membership carries a role: | Administrator | Yes | Yes | | Stakeholder | Yes | No (read-only) | -To grant an existing user access to another project, add a **Membership** (Admin +To grant an existing user access to another scenario, add a **Membership** (Admin → Memberships → Add) with the appropriate role, or send them an invitation to -that project. +that scenario. ## Forgotten passwords @@ -61,7 +61,7 @@ delivery being configured. ## Removing access -- Delete a user's **Membership** to remove their access to a single project. +- Delete a user's **Membership** to remove their access to a single scenario. - Deactivate or delete the **User** in the admin to revoke access entirely. - A signed-in user can export or delete **their own** account and content from the account page (`/me/`). diff --git a/docs/decisions/adrs/001-front-end-product-readiness-boundaries.md b/docs/decisions/adrs/001-front-end-product-readiness-boundaries.md index b96198f..69fa07e 100644 --- a/docs/decisions/adrs/001-front-end-product-readiness-boundaries.md +++ b/docs/decisions/adrs/001-front-end-product-readiness-boundaries.md @@ -14,7 +14,7 @@ The repository already has important boundaries that must remain intact: - ACES packs and review bundles are the source of scenario content; the workbench database owns collaboration state only. -- Project membership is the authorization boundary, enforced in +- Scenario membership is the authorization boundary, enforced in `workbench.access` and `workbench.authz`. - Account creation is invite-only through the existing accounts app. - Security posture is centralized in Django settings, middleware, CSP, @@ -72,7 +72,7 @@ Security gates the implementation must pass: - Authentication: reuse Django auth views, the custom email user model, password validators, django-axes, and django-ratelimit. Do not create open registration or parallel login/reset flows. -- Authorization: use `login_required`, `access.member_project`, +- Authorization: use `login_required`, `access.member_scenario`, `access.scoped_revision`, and `authz.can_contribute`; hiding links is not authorization. - CSRF and destructive actions: account deletion and all state-changing forms @@ -99,7 +99,7 @@ Canonical incumbents to build on: - Readiness reporting in `workbench.management.commands.doctor`. - URL names from `accounts.urls`, `workbench.urls`, and Django `admin:index`. - Account forms/views in `accounts.forms` and `accounts.views`. -- Project authorization helpers in `workbench.access` and `workbench.authz`. +- Scenario authorization helpers in `workbench.access` and `workbench.authz`. - Collaboration context/actions in `workbench.collab`. - Projection parsing/import rules in `workbench.ingest`. - Existing test suites for hardening, auth, accounts, review views, collab, diff --git a/docs/go-live-checklist.md b/docs/go-live-checklist.md index 1e6f5cc..1eb03c8 100644 --- a/docs/go-live-checklist.md +++ b/docs/go-live-checklist.md @@ -31,7 +31,7 @@ Work through this before exposing an instance to the internet. See - [ ] Migrations are applied (`aces-workbench migrate`). - [ ] The first administrator exists (`aces-workbench createadmin`). - [ ] The configured Django admin path is restricted to administrators. -- [ ] Sign in over HTTPS, create a project, and send yourself an invitation to +- [ ] Sign in over HTTPS, create a scenario, and send yourself an invitation to confirm email delivery end to end. ## Operate diff --git a/docs/index.md b/docs/index.md index 68518de..247e164 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,7 +17,7 @@ writes changes back into a pack. hosting. Check this first so nothing blocks you halfway through. - **[Setup](setup.md)** — install to a working, multi-user workspace. - **[Access & registration](access-control.md)** — the invite-only account model - and project roles. + and scenario roles. - **[Deployment](deployment.md)** — container, configuration, and hosting. - **[Go-live checklist](go-live-checklist.md)** — the final checks before you expose an instance to the internet. diff --git a/docs/product-intent.md b/docs/product-intent.md index 5cfe8a0..6ebfca1 100644 --- a/docs/product-intent.md +++ b/docs/product-intent.md @@ -74,12 +74,12 @@ Hiding navigation controls in the browser is not authorization. Access events an review-state changes should be auditable. The initial roles are `author`, `reviewer`, `stakeholder`, and `administrator`. -Authorization must be enforceable per scenario or project, not only per deployment. +Authorization must be enforceable per scenario, not only per deployment. ## Initial Implementation Slices 1. Define and validate a versioned review-bundle schema from a sample ATLAS projection. -2. Select the application stack and establish authenticated users and projects. +2. Select the application stack and establish authenticated users and scenarios. 3. Ingest immutable scenario revisions and render the prototype overview. 4. Add stable detail routes for every object and relationship type. 5. Add anchored comments, review states, and an auditable activity stream. diff --git a/docs/setup.md b/docs/setup.md index 6f13731..78c6c2f 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -35,16 +35,16 @@ debug status so you can see what still needs configuration. Set `ACES_WORKBENCH_SECRET_KEY` to a stable value; without it a random key is used and sessions reset on every restart. -## 3. Create projects and add people +## 3. Create scenarios and add people Sign in at `/accounts/login/` and open the Django admin at `/control/` unless you changed `ACES_WORKBENCH_ADMIN_PATH`. -- **Create a project**: Admin → Projects → Add. +- **Create a scenario**: Admin → Scenarios → Add. - **Add a member you can manage directly**: Admin → Users → Add (this sets a - real password), then add a Membership on the project with a role + real password), then add a Membership on the scenario with a role (`author`, `reviewer`, `stakeholder`, `administrator`). -- **Invite by email**: Admin → Invitations → Add (email, project, role). An +- **Invite by email**: Admin → Invitations → Add (email, scenario, role). An invitation email with an accept link is sent on save; the invitee sets their own password. Use the "Resend invitation email" action to send it again. @@ -55,7 +55,7 @@ from there for local use. Configure SMTP (below) to deliver for real. ## 4. Import a scenario ```bash -aces-workbench import /path/to/pack --project +aces-workbench import /path/to/pack --scenario ``` Point it at a pack directory containing an ATLAS technique projection, or at the diff --git a/fixtures/sample-scenario/assets/affordances.yaml b/fixtures/sample-scenario/assets/affordances.yaml new file mode 100644 index 0000000..8c6bfc8 --- /dev/null +++ b/fixtures/sample-scenario/assets/affordances.yaml @@ -0,0 +1,9 @@ +schema_version: 1 +pack: sample-scenario +source_oracle: oracle/objectives.yaml +affordances: + - id: proof-sink + title: Proof sink + type: evidence + asset: proof-service + description: Receives synthetic evidence receipts. diff --git a/fixtures/sample-scenario/assets/planned-assets.yaml b/fixtures/sample-scenario/assets/planned-assets.yaml new file mode 100644 index 0000000..1818890 --- /dev/null +++ b/fixtures/sample-scenario/assets/planned-assets.yaml @@ -0,0 +1,12 @@ +schema_version: 1 +pack: sample-scenario +description: Minimal planned asset fixture. +asset_sets: + - asset_id: proof-service + category: evidence + implementation_status: implemented_source + paths: [fixtures/sample-scenario/oracle/telemetry.yaml] + visibility: participant-visible + source_refs: [proof-api] + topology_refs: [proof-service] + implementation_plan: Keep proof events synthetic for fixture tests. diff --git a/fixtures/sample-scenario/assets/services/sample-runtime/app.py b/fixtures/sample-scenario/assets/services/sample-runtime/app.py new file mode 100644 index 0000000..2b8f966 --- /dev/null +++ b/fixtures/sample-scenario/assets/services/sample-runtime/app.py @@ -0,0 +1,3 @@ +INFER_PATH = "/v1/infer" + +QUICK_CHALLENGES = ({"id": "recon", "title": "Recon", "entrypoint": INFER_PATH},) diff --git a/fixtures/sample-scenario/challenges/challenges.yaml b/fixtures/sample-scenario/challenges/challenges.yaml new file mode 100644 index 0000000..be92d59 --- /dev/null +++ b/fixtures/sample-scenario/challenges/challenges.yaml @@ -0,0 +1,20 @@ +schema_version: 1 +pack: sample-scenario +challenges: + - flag_id: flag-recon + title: Recon Receipt + category: Model Security + difficulty: easy + points: 100 + question: Submit evidence that the reconnaissance behavior was completed. + hints: + - Compare the candidate action with the expected evidence event. + - The receipt must be tied to the participant namespace. + - flag_id: flag-evasion + title: Evasion Receipt + category: Model Security + difficulty: medium + points: 150 + question: Submit evidence that the evasion behavior is planned. + hints: + - This challenge is planned but not runtime-backed in the sample fixture. diff --git a/fixtures/sample-scenario/design/software-inventory.yaml b/fixtures/sample-scenario/design/software-inventory.yaml new file mode 100644 index 0000000..6080701 --- /dev/null +++ b/fixtures/sample-scenario/design/software-inventory.yaml @@ -0,0 +1,9 @@ +schema_version: 1 +components: + - component_id: proof-api + topology_refs: [proof-service] + upstream: local + operating_mode: synthetic + profiles: [local] + authenticity: fixture + path_critical: true diff --git a/fixtures/sample-scenario/design/topology.yaml b/fixtures/sample-scenario/design/topology.yaml new file mode 100644 index 0000000..4e34bda --- /dev/null +++ b/fixtures/sample-scenario/design/topology.yaml @@ -0,0 +1,41 @@ +schema_version: 1 +name: sample-topology +profiles: + local: {} +zones: + - name: participant + kind: user +networks: + - name: lab + zone: participant +assets: + - id: proof-service + hostname: proof.local + asset_type: service-host + role: evidence collector + zone: participant + networks: [lab] + software_component: proof-api + visibility: participant-visible + reset_owner: proof-service + implementation_status: implemented + description: Receives synthetic proof events. +services: + - id: proof-api + asset: proof-service + ports: [8080] + software_component: proof-api + visibility: participant-visible + reset_owner: proof-service + description: Proof API endpoint. +applications: [] +datasets: [] +artifacts: [] +path_objectives: + - id: obj-sample + title: Sample path + success_states: [S-RECON, S-EVASION] +validation_flows: + - id: validate-sample + command: validation/validate_contract.py + covers: [obj-sample] diff --git a/fixtures/sample-scenario/flags/placement.yaml b/fixtures/sample-scenario/flags/placement.yaml new file mode 100644 index 0000000..573e95b --- /dev/null +++ b/fixtures/sample-scenario/flags/placement.yaml @@ -0,0 +1,29 @@ +schema_version: 1 +pack: sample-scenario +source_scoring: oracle/scoring.yaml +namespace: [range_instance, participant] +flags: + - flag_id: flag-recon + outcome: recon + source: instructions + instructions: docs/attack-path.md#module-1 + profiles: [local] + evidence: ev-recon + delivery: + kind: verdict_response + asset: proof-service + service: proof-api + affordance: proof-sink + reset_owner: proof-service + - flag_id: flag-evasion + outcome: evasion + source: instructions + instructions: docs/attack-path.md#module-2 + profiles: [local] + evidence: ev-evasion + delivery: + kind: verdict_response + asset: proof-service + service: proof-api + affordance: proof-sink + reset_owner: proof-service diff --git a/fixtures/sample-scenario/oracle/objectives.yaml b/fixtures/sample-scenario/oracle/objectives.yaml new file mode 100644 index 0000000..ba4b58b --- /dev/null +++ b/fixtures/sample-scenario/oracle/objectives.yaml @@ -0,0 +1,47 @@ +schema_version: 1 +oracle_id: sample-scenario-oracle +scenario: {pack: sample-scenario, version: test} +path_steps: + - id: "1" + title: Reconnaissance behavior + success_state: S-RECON + prerequisites: [] + classification: quick + required_evidence: + - id: ev-recon + predicate: Read-only evidence confirms the reconnaissance behavior occurred. + source: docs/attack-path.md#module-1 + proof_fields: [actor_role, asset_id, event_kind, outcome_id, range_instance, participant, timestamp, status, digest] + optional_evidence: [] + failure_states: [] + - id: "2" + title: Evasion behavior + success_state: S-EVASION + prerequisites: [] + classification: intermediate + required_evidence: + - id: ev-evasion + predicate: Evidence confirms the evasion behavior is contractually planned. + source: docs/attack-path.md#module-2 + proof_fields: [actor_role, asset_id, event_kind, outcome_id, range_instance, participant, timestamp, status, digest] + optional_evidence: [] + failure_states: [] +outcomes: + - id: recon + title: Reconnaissance behavior completed + required: false + success_state: S-RECON + canonical_steps: ["1"] + required_evidence: [ev-recon] + optional_evidence: [] + failure_states: [] + awards: [{consumer: native, adapter: native:recon, points: 100}] + - id: evasion + title: Evasion behavior planned + required: false + success_state: S-EVASION + canonical_steps: ["2"] + required_evidence: [ev-evasion] + optional_evidence: [] + failure_states: [] + awards: [{consumer: native, adapter: native:evasion, points: 150}] diff --git a/fixtures/sample-scenario/oracle/scoring.yaml b/fixtures/sample-scenario/oracle/scoring.yaml new file mode 100644 index 0000000..a9ddfb5 --- /dev/null +++ b/fixtures/sample-scenario/oracle/scoring.yaml @@ -0,0 +1,20 @@ +schema_version: 1 +scoring_id: sample-scenario-scoring +source_oracle: oracle/objectives.yaml +mode: objective_projection +max_points: 250 +awards: + - id: recon + points: 100 + evidence: [ev-recon] + description: Reconnaissance behavior completed. + - id: evasion + points: 150 + evidence: [ev-evasion] + description: Evasion behavior planned. +alternate_awards: [] +bundles: + - id: sample-path + title: Sample path + outcomes: [recon, evasion] + points: 250 diff --git a/fixtures/sample-scenario/oracle/telemetry.yaml b/fixtures/sample-scenario/oracle/telemetry.yaml new file mode 100644 index 0000000..029d990 --- /dev/null +++ b/fixtures/sample-scenario/oracle/telemetry.yaml @@ -0,0 +1,34 @@ +schema_version: 1 +telemetry_id: sample-scenario-telemetry +source_oracle: oracle/objectives.yaml +namespace: [range_instance, participant] +sink_service: proof-api +safe_fields: + - actor_role + - asset_id + - digest + - event_kind + - outcome_id + - participant + - range_instance + - status + - timestamp +forbidden_fields: [answer, credential, flag, prompt, raw_completion] +events: + - id: event-recon + evidence: ev-recon + event_kind: recon_verdict + source_service: proof-api + source_asset: proof-service + fields: [actor_role, asset_id, event_kind, outcome_id, range_instance, participant, timestamp, status, digest] + freshness_seconds: 3600 + reset_owner: proof-service + - id: event-evasion + evidence: ev-evasion + event_kind: evasion_verdict + source_service: proof-api + source_asset: proof-service + fields: [actor_role, asset_id, event_kind, outcome_id, range_instance, participant, timestamp, status, digest] + freshness_seconds: 3600 + reset_owner: proof-service +negative_gates: [] diff --git a/fixtures/sample-scenario/sdl/sample-scenario.sdl.yaml b/fixtures/sample-scenario/sdl/sample-scenario.sdl.yaml new file mode 100644 index 0000000..6f248ff --- /dev/null +++ b/fixtures/sample-scenario/sdl/sample-scenario.sdl.yaml @@ -0,0 +1,44 @@ +name: sample-scenario +version: 1.0.0 +description: Minimal SDL fixture used as the authoritative topology source in tests. +nodes: + participant-workstation: + type: VM + os: linux + os_version: Debian 12 + resources: { ram: 2 GiB, cpu: 1 } + description: Participant workstation. + services: + - name: browser-terminal-ssh + port: 22 + proof-service: + type: VM + os: linux + os_version: Debian 12 + resources: { ram: 1 GiB, cpu: 1 } + description: Synthetic proof service. + services: + - name: proof-api + port: 8080 +entities: + participant-team: + role: red + description: Authorized participant team. +agents: + participant: + entity: participant-team + description: Offensive participant. + initial-knowledge: + hosts: [participant-workstation, proof-service] + services: [browser-terminal-ssh, proof-api] +behavior-specifications: + module-01-recon: + semantic-version: 1.0.0 + lifecycle-state: draft + participant-refs: [participant] + ai-offensive-behavior-refs: [reconnaissance] + module-02-evasion: + semantic-version: 1.0.0 + lifecycle-state: draft + participant-refs: [participant] + ai-offensive-behavior-refs: [defense-evasion] diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..e41de02 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1529 @@ +{ + "name": "aces-workbench-spa", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "aces-workbench-spa", + "version": "0.1.0", + "dependencies": { + "@fontsource-variable/geist": "^5.2.9", + "@fontsource-variable/geist-mono": "^5.2.8", + "@tanstack/react-query": "^5.62.7", + "clsx": "^2.1.1", + "lucide-react": "^1.24.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^7.1.1", + "tailwind-merge": "^3.6.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.2", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^6.0.3", + "tailwindcss": "^4.3.2", + "typescript": "^5.7.2", + "vite": "^8.1.4" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fontsource-variable/geist": { + "version": "5.2.9", + "resolved": "https://registry.npmjs.org/@fontsource-variable/geist/-/geist-5.2.9.tgz", + "integrity": "sha512-TP+QSBG3wxKGPE33CbMy/L0Nu3qvJ6Fy81Yc4LnQ95xH+i+cfEp8fyU8/kfV14YwszxIFPhnoMTbjL71waVpyQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/geist-mono": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/geist-mono/-/geist-mono-5.2.8.tgz", + "integrity": "sha512-KI5bj+hkkRiHttYHmccotUZ80ZuZyai+RwI1d7UId0clkx/jXxlo8qYK8j54WzmpBjtMoEMPyllV7faDcj+6RA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "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/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", + "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", + "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-x64": "4.3.2", + "@tailwindcss/oxide-freebsd-x64": "4.3.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-x64-musl": "4.3.2", + "@tailwindcss/oxide-wasm32-wasi": "4.3.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", + "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", + "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", + "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", + "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", + "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", + "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", + "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", + "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", + "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", + "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", + "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", + "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz", + "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.2", + "@tailwindcss/oxide": "4.3.2", + "tailwindcss": "4.3.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.101.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.2.tgz", + "integrity": "sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.2.tgz", + "integrity": "sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/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.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "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": "6.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", + "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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/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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/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/lucide-react": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.24.0.tgz", + "integrity": "sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.17.tgz", + "integrity": "sha512-J7EF+8X+CzRPaJPOv9Ck2wNWJvGnnl3PcNPAdGg6GTLjyVpyQ0yATMSXRFRV01BviT/9Gwuc3rjEyJbDJG9a4w==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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-router": { + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz", + "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz", + "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/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/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", + "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "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/vite": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz", + "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.16", + "rolldown": "~1.1.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..9a54956 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,31 @@ +{ + "name": "aces-workbench-spa", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "build": "tsc --noEmit && vite build", + "dev": "vite --host 0.0.0.0", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@fontsource-variable/geist": "^5.2.9", + "@fontsource-variable/geist-mono": "^5.2.8", + "@tanstack/react-query": "^5.62.7", + "clsx": "^2.1.1", + "lucide-react": "^1.24.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^7.1.1", + "tailwind-merge": "^3.6.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.2", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^6.0.3", + "tailwindcss": "^4.3.2", + "typescript": "^5.7.2", + "vite": "^8.1.4" + } +} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..3f043bf --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,507 @@ +export type Principal = { + email: string; + displayName: string; + isStaff: boolean; +}; + +export type ScenarioRow = { + slug: string; + name: string; + description: string; + role: string; + revisionCount: number; + updatedAt: string; +}; + +export type ScenarioDetail = { + scenario: ScenarioRow; + revisions: Array<{ + id: number; + label: string; + packVersion: string; + framework: string; + createdAt: string; + updatedAt: string; + moduleCount: number; + techniqueCount: number; + evidenceCount: number; + commentCount: number; + decisionCount: number; + challengeCount: number; + }>; +}; + +export type RevisionWorkspace = { + id: number; + label: string; + scenario: { slug: string; name: string; description: string }; + framework: string; + createdAt: string; + summary: { + totalMinutes: number; + moduleCount: number; + techniqueCount: number; + evidenceCount: number; + challengeCount: number; + implementedChallengeCount: number; + plannedChallengeCount: number; + }; + schedule: { + total_minutes?: number; + module_count?: number; + outcome_count?: number; + }; + scoring: { + mode?: string; + max_points?: number | null; + awards?: Array<{ + id: string; + points: number | null; + evidence: string[]; + required_outcomes: string[]; + description: string; + }>; + alternate_awards?: Array<{ + id: string; + points: number | null; + evidence: string[]; + required_outcomes: string[]; + description: string; + }>; + bundles?: Array<{ + id: string; + title: string; + description: string; + outcomes: string[]; + points: number | null; + }>; + }; + environment: { + counts?: Record; + assets?: EnvironmentAsset[]; + services?: EnvironmentService[]; + applications?: EnvironmentApplication[]; + datasets?: EnvironmentDataset[]; + artifacts?: EnvironmentArtifact[]; + path_objectives?: EnvironmentPathObjective[]; + validation_flows?: EnvironmentValidationFlow[]; + software_components?: EnvironmentSoftwareComponent[]; + planned_assets?: EnvironmentPlannedAsset[]; + affordances?: EnvironmentAffordance[]; + sdl_behavior_specs?: Array<{ id: string; title: string }>; + }; + telemetry: { + sink_service?: string; + safe_fields?: string[]; + forbidden_fields?: string[]; + negative_gates?: Array<{ id: string; description: string }>; + }; + topology: { + source?: string; + name?: string; + version?: string; + description?: string; + infrastructure?: TopologyInfrastructure[]; + relationships?: TopologyRelationship[]; + nodes?: TopologyNode[]; + entities?: TopologyEntity[]; + agents?: TopologyAgent[]; + zones?: TopologyZone[]; + networks?: TopologyNetwork[]; + links?: Array<{ source: string; target: string; type: string }>; + coverage?: { + sdl_node_count: number; + sdl_infrastructure_count: number; + sdl_relationship_count: number; + sdl_service_count: number; + sdl_agent_count: number; + sdl_entity_count: number; + contract_asset_count: number; + contract_service_count: number; + contract_assets_missing_from_sdl: EnvironmentAsset[]; + contract_services_missing_from_sdl: EnvironmentService[]; + }; + }; + modules: Array<{ + id: string; + name: string; + behaviorSpecification: string; + tier: string; + objective: string; + minutes: number | null; + flagOutcome: string; + justification: string; + techniqueCount: number; + evidenceCount: number; + commentCount: number; + decisionCount: number; + }>; + techniques: Array<{ + id: string; + name: string; + module: string; + tactics: string[]; + evidence: string; + surface: string; + relationship: string; + coverageStatus: string; + plannedAction: string; + rationale: string; + commentCount: number; + decisionCount: number; + }>; + evidence: Array<{ + id: string; + description: string; + techniqueCount: number; + commentCount: number; + decisionCount: number; + }>; + challenges: Array<{ + id: string; + flagId: string; + outcome: string; + title: string; + question: string; + category: string; + difficulty: string; + points: number | null; + hints: string[]; + implemented: boolean; + status: string; + runtimeEntrypoint: string; + sourcePath: string; + module: string; + moduleName: string; + techniqueIds: string[]; + canonicalSteps: string[]; + readiness: Record; + scoring: { + id?: string; + points?: number | null; + evidence?: string[]; + required_outcomes?: string[]; + description?: string; + }; + alternateAwards: Array<{ + id: string; + points: number | null; + evidence: string[]; + required_outcomes: string[]; + description: string; + }>; + bundles: Array<{ + id: string; + title: string; + description: string; + outcomes: string[]; + points: number | null; + }>; + delivery: Record; + evidenceRequirements: Array<{ + evidenceId: string; + predicate: string; + sourcePath: string; + eventId: string; + eventKind: string; + sourceService: string; + sourceAsset: string; + freshnessSeconds: number | null; + resetOwner: string; + fields: string[]; + proofFields: string[]; + }>; + commentCount: number; + decisionCount: number; + }>; + decisions: Array<{ + id: number; + objectType: string; + objectId: string; + decision: string; + rationale: string; + author: string; + createdAt: string; + }>; + comments: Array<{ + id: number; + objectType: string; + objectId: string; + body: string; + author: string; + createdAt: string; + updatedAt: string; + edited: boolean; + }>; +}; + +export type EnvironmentAsset = { + id: string; + hostname: string; + asset_type: string; + role: string; + zone: string; + networks: string[]; + software_component: string; + visibility: string; + reset_owner: string; + implementation_status: string; + description: string; +}; + +export type EnvironmentService = { + id: string; + asset: string; + ports: string[]; + software_component: string; + visibility: string; + reset_owner: string; + description: string; +}; + +export type EnvironmentApplication = { + id: string; + asset: string; + app_type: string; + software_component: string; + auth_service: string; + visibility: string; + reset_owner: string; + description: string; +}; + +export type EnvironmentDataset = { + id: string; + kind: string; + locations: string[]; + synthetic: boolean; + visibility: string; + reset_owner: string; + description: string; +}; + +export type EnvironmentArtifact = { + id: string; + kind: string; + asset: string; + visibility: string; + reset_owner: string; + secret_handling: string; + description: string; +}; + +export type EnvironmentPathObjective = { + id: string; + title: string; + success_states: string[]; + build_targets: string[]; + test_targets: string[]; + walkthrough_targets: string[]; +}; + +export type EnvironmentValidationFlow = { + id: string; + command: string; + covers: string[]; +}; + +export type EnvironmentSoftwareComponent = { + id: string; + topology_refs: string[]; + upstream: string; + operating_mode: string; + profiles: string[]; + authenticity: string; + path_critical: boolean; +}; + +export type EnvironmentPlannedAsset = { + id: string; + category: string; + implementation_status: string; + paths: string[]; + visibility: string; + source_refs: string[]; + topology_refs: string[]; + implementation_plan: string; +}; + +export type EnvironmentAffordance = { + id: string; + title: string; + type: string; + asset: string; + description: string; +}; + +export type TopologyInfrastructure = { + id: string; + type: string; + count: number | null; + cidr: string; + gateway: string; + internal: boolean; + description: string; + properties: Record; +}; + +export type TopologyRelationship = { + id: string; + type: string; + source: string; + target: string; + ports: string; + category: string; + properties: Record; +}; + +export type TopologyNode = { + id: string; + type: string; + os: string; + os_version: string; + resources: Record; + description: string; + services: TopologyService[]; + zone: string; + networks: string[]; + role: string; + visibility: string; + implementation_status: string; +}; + +export type TopologyService = { + id: string; + name: string; + port: number | null; + asset: string; + software_component: string; + visibility: string; + reset_owner: string; + description: string; +}; + +export type TopologyEntity = { + id: string; + role: string; + description: string; +}; + +export type TopologyAgent = { + id: string; + entity: string; + description: string; + initial_hosts: string[]; + initial_services: string[]; +}; + +export type TopologyZone = { + name: string; + kind: string; + networks: string[]; + description: string; +}; + +export type TopologyNetwork = { + name: string; + zone: string; + scope: string; + isolation: string; + providers: string[]; + routes: string[]; +}; + +export type DecisionValue = "accept" | "needs-change" | "resolve" | "reopen"; +export type RevisionComment = RevisionWorkspace["comments"][number]; +export type RevisionDecision = RevisionWorkspace["decisions"][number]; + +async function getJson(path: string): Promise { + const response = await fetch(path, { + credentials: "same-origin", + headers: { Accept: "application/json" }, + }); + if (!response.ok) { + throw new Error(`Request failed (${response.status})`); + } + return response.json() as Promise; +} + +async function postJson(path: string, payload: unknown): Promise { + const csrfToken = getCookie("csrftoken"); + const response = await fetch(path, { + method: "POST", + credentials: "same-origin", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + ...(csrfToken ? { "X-CSRFToken": csrfToken } : {}), + }, + body: JSON.stringify(payload), + }); + if (!response.ok) { + let detail = `Request failed (${response.status})`; + try { + const body = (await response.json()) as { detail?: string }; + if (body.detail) detail = body.detail; + } catch { + // Keep the status-only fallback. + } + throw new Error(detail); + } + return response.json() as Promise; +} + +function getCookie(name: string) { + const match = document.cookie + .split("; ") + .find((candidate) => candidate.startsWith(`${encodeURIComponent(name)}=`)); + if (!match) return ""; + return decodeURIComponent(match.split("=").slice(1).join("=")); +} + +function objectActivityPath(revisionId: string | number, objectType: string, objectId: string, suffix: string) { + return `/api/app/revisions/${encodeURIComponent(String(revisionId))}/objects/${encodeURIComponent( + objectType, + )}/${encodeURIComponent(objectId)}/${suffix}`; +} + +export function getPrincipal() { + return getJson("/api/app/me"); +} + +export function getScenarios() { + return getJson<{ scenarios: ScenarioRow[] }>("/api/app/scenarios"); +} + +export function getScenario(slug: string) { + return getJson(`/api/app/scenarios/${encodeURIComponent(slug)}`); +} + +export function getRevision(id: string) { + return getJson(`/api/app/revisions/${encodeURIComponent(id)}`); +} + +export function postObjectComment( + revisionId: string | number, + objectType: string, + objectId: string, + body: string, +) { + return postJson<{ comment: RevisionComment }>( + objectActivityPath(revisionId, objectType, objectId, "comments"), + { body }, + ); +} + +export function postObjectDecision( + revisionId: string | number, + objectType: string, + objectId: string, + decision: DecisionValue, + rationale: string, +) { + return postJson<{ decision: RevisionDecision }>( + objectActivityPath(revisionId, objectType, objectId, "decisions"), + { decision, rationale }, + ); +} diff --git a/frontend/src/components/AppShell.tsx b/frontend/src/components/AppShell.tsx new file mode 100644 index 0000000..da1f9af --- /dev/null +++ b/frontend/src/components/AppShell.tsx @@ -0,0 +1,135 @@ +import { useEffect, useRef, type ReactNode } from "react"; +import { Link, NavLink, useLocation } from "react-router-dom"; +import { BookOpen, FolderKanban, LogOut, ShieldCheck, UserRound } from "lucide-react"; +import { useQuery } from "@tanstack/react-query"; + +import { getPrincipal } from "@/api/client"; +import { Button } from "@/components/ui"; +import { cn } from "@/lib/utils"; + +function ShellLink({ + to, + children, +}: Readonly<{ + to: string; + children: ReactNode; +}>) { + return ( + + cn( + "flex items-center gap-2.5 rounded-md px-2 py-1.5 text-sm transition-colors", + isActive + ? "bg-accent text-accent-foreground" + : "text-muted-foreground hover:bg-accent/60 hover:text-foreground", + ) + } + > + {children} + + ); +} + +export function AppShell({ children }: Readonly<{ children: ReactNode }>) { + const location = useLocation(); + const mainRef = useRef(null); + const firstRender = useRef(true); + const principal = useQuery({ queryKey: ["principal"], queryFn: getPrincipal }); + + useEffect(() => { + document.documentElement.classList.add("dark"); + }, []); + + useEffect(() => { + if (firstRender.current) { + firstRender.current = false; + return; + } + mainRef.current?.focus(); + }, [location.pathname]); + + return ( +
+ + Skip to content + +
+ + +
+
+ + ACES Workbench + +
+ + {principal.data?.displayName || principal.data?.email || "Signed in"} + +
+ + +
+
+
+ {children} +
+
+
+
+ ); +} + +function getCookie(name: string) { + const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`)); + return match ? decodeURIComponent(match[1]) : ""; +} diff --git a/frontend/src/components/ui.tsx b/frontend/src/components/ui.tsx new file mode 100644 index 0000000..fa329fb --- /dev/null +++ b/frontend/src/components/ui.tsx @@ -0,0 +1,165 @@ +import type { + ButtonHTMLAttributes, + HTMLAttributes, + KeyboardEvent, + MouseEvent, + ReactNode, +} from "react"; +import { Link, useNavigate, type LinkProps } from "react-router-dom"; + +import { cn } from "@/lib/utils"; + +export function Card({ className, ...props }: HTMLAttributes) { + return ( +
+ ); +} + +export function Button({ className, ...props }: ButtonHTMLAttributes) { + return ( + - + {% endblock %} {% block content %} @@ -38,8 +37,8 @@

Coverage overview

-
{{ counts.techniques }}Pinned techniques
-
{{ counts.tactics }}Governed tactics
+
{{ counts.techniques }}Pinned behaviors
+
{{ counts.tactics }}Behavior refs
{{ counts.steps }}Selectable modules
{% if shortest_minutes %}{{ shortest_minutes }} min{% else %}—{% endif %} @@ -49,17 +48,17 @@

Coverage overview

-

Technique relationships by tactic

+

Behavior relationships by ref

{% for tactic in tactics %} - {% url 'tactic-detail' project.slug scenario.slug revision.pk tactic.tactic_id as tactic_url %} + {% url 'tactic-detail' scenario.slug revision.pk tactic.tactic_id as tactic_url %} {{ tactic.name }} - + {{ tactic.technique_count }} {% empty %} -

No tactics in this revision.

+

No behavior refs in this revision.

{% endfor %}
@@ -68,36 +67,29 @@

Technique relationships by tactic

Challenge progression

{% for step in steps %} - {% url 'step-detail' project.slug scenario.slug revision.pk step.path_step as step_url %} + {% url 'step-detail' scenario.slug revision.pk step.path_step as step_url %} {{ step.path_step }}. {{ step.surface|default:"Module" }} {% if step.estimated_minutes %}{{ step.estimated_minutes }} min{% else %}Unestimated{% endif %} - / {{ step.technique_count }} technique{{ step.technique_count|pluralize }} + / {{ step.technique_count }} behavior{{ step.technique_count|pluralize }} {% empty %}

No modules in this revision.

{% endfor %}
- {% if integrity_fields %} -
- {% for label, value in integrity_fields %} -
{{ label }}{{ value }}
- {% endfor %} -
- {% endif %}