From fc7ba0846fbe1e57cb9f1ef9eb30b564d5dedf33 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Wed, 18 Mar 2026 02:06:10 +0000 Subject: [PATCH 1/7] docs: add design for security headers, coverage gates, and web vitals Co-Authored-By: Claude Opus 4.6 (1M context) --- ...18-security-coverage-performance-design.md | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 docs/plans/2026-03-18-security-coverage-performance-design.md diff --git a/docs/plans/2026-03-18-security-coverage-performance-design.md b/docs/plans/2026-03-18-security-coverage-performance-design.md new file mode 100644 index 00000000..88aefda8 --- /dev/null +++ b/docs/plans/2026-03-18-security-coverage-performance-design.md @@ -0,0 +1,116 @@ +# Security Headers, Coverage Gates & Web Vitals Design + +> **Date:** 2026-03-18 +> **Product:** ResolutionFlow +> **Branch:** `feat/security-headers-coverage-performance` +> **Purpose:** Add HTTP security headers, enforce test coverage gates, and instrument Core Web Vitals reporting + +--- + +## Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Security headers priority | First | Highest trust signal for MSP buyers, real protection | +| CSP rollout strategy | Report-only first, then enforce | Avoids breaking third-party integrations (PostHog, Sentry, Google Fonts, React Flow) | +| Backend coverage gate | 80% fail threshold | Already near this level, prevents drift | +| Frontend coverage gate | Report-only (no gate yet) | Starting from zero — establish baseline first | +| Web Vitals destination | PostHog | 100% of sessions captured (vs Sentry's 20% sample), correlate with product analytics | + +--- + +## 1. Security Headers Middleware + +### New file: `backend/app/core/security_headers.py` + +Starlette middleware that adds security headers to every response. + +### Headers + +| Header | Value | Purpose | +|--------|-------|---------| +| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | Force HTTPS for 1 year | +| `X-Content-Type-Options` | `nosniff` | Prevent MIME sniffing | +| `X-Frame-Options` | `DENY` | Block iframe embedding | +| `Referrer-Policy` | `strict-origin-when-cross-origin` | Limit referrer leakage | +| `Permissions-Policy` | `camera=(), microphone=(), geolocation=()` | Disable unused browser APIs | +| `Content-Security-Policy-Report-Only` | *(see below)* | CSP in report-only mode | + +### CSP Directive (report-only) + +``` +default-src 'self'; +script-src 'self' https://us.i.posthog.com https://*.sentry.io; +style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; +font-src 'self' https://fonts.gstatic.com; +img-src 'self' data: blob:; +connect-src 'self' https://us.posthog.com https://us.i.posthog.com https://*.sentry.io https://api.resolutionflow.com; +frame-ancestors 'none'; +base-uri 'self'; +form-action 'self' +``` + +### Wiring + +- Add middleware in `main.py` after CORS middleware (so CORS preflight responses aren't affected) +- CSP directives configurable via `config.py` so promotion to enforcing mode is a config change, not a code change +- HSTS only sent when `DEBUG=false` (avoid locking localhost into HTTPS) + +### Tests + +Integration test that hits an endpoint and asserts all expected headers are present with correct values. + +--- + +## 2. Coverage Gates + +### Backend: Enforce at 80% + +- Add `--cov-fail-under=80` to the pytest command in CI +- One-line change — reporting already wired up with `pytest-cov` + +### Frontend: Report-only (establish baseline) + +- Install `@vitest/coverage-v8` as dev dependency +- Add coverage config to `vite.config.ts`: + - Reporters: `text` + `json-summary` + `html` + - Include: `src/**/*.{ts,tsx}` + - Exclude: `src/test/`, `src/types/`, `**/*.d.ts` +- Add `test:coverage` script to `package.json` +- Update CI to run `npm run test:coverage` instead of `npm test` +- Display summary in CI output — no failure threshold yet +- Add `coverage/` to `.gitignore` + +--- + +## 3. Web Vitals → PostHog + +### Install + +`web-vitals` npm package. + +### New file: `frontend/src/lib/webVitals.ts` + +- Import `onLCP`, `onINP`, `onCLS`, `onFCP`, `onTTFB` from `web-vitals` +- Each callback sends a PostHog event (`web_vitals`) with properties: + - `metric_name` — LCP, INP, CLS, FCP, TTFB + - `metric_value` — numeric value + - `metric_rating` — good / needs-improvement / poor + - `page_path` — current route +- Single `initWebVitals()` function that registers all observers + +### Wiring + +Call `initWebVitals()` in `main.tsx` after PostHog initialization. + +--- + +## Scope Summary + +| Area | Scope | Files | +|------|-------|-------| +| Security headers | New middleware + config + test | 3-4 backend files | +| Coverage gates | CI config + vitest coverage setup | CI workflow + 3 frontend config files | +| Web Vitals | New lib + dependency + main.tsx wiring | 2-3 frontend files | + +Small, contained changes across all three. No architectural changes or new database models. From 24acfc9a45c7a1147907aa784080d801ddbd6536 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Wed, 18 Mar 2026 02:07:58 +0000 Subject: [PATCH 2/7] docs: add implementation plan for security headers, coverage, and web vitals Co-Authored-By: Claude Opus 4.6 (1M context) --- ...026-03-18-security-coverage-performance.md | 466 ++++++++++++++++++ 1 file changed, 466 insertions(+) create mode 100644 docs/plans/2026-03-18-security-coverage-performance.md diff --git a/docs/plans/2026-03-18-security-coverage-performance.md b/docs/plans/2026-03-18-security-coverage-performance.md new file mode 100644 index 00000000..ebe4ee94 --- /dev/null +++ b/docs/plans/2026-03-18-security-coverage-performance.md @@ -0,0 +1,466 @@ +# Security Headers, Coverage Gates & Web Vitals Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add HTTP security headers to every response, enforce backend test coverage at 80%, add frontend coverage reporting, and instrument Core Web Vitals via PostHog. + +**Architecture:** Three independent workstreams — (1) new Starlette middleware for security headers with configurable CSP, (2) CI pipeline updates for coverage gates, (3) new frontend lib for web-vitals → PostHog. No database changes, no new API endpoints. + +**Tech Stack:** FastAPI/Starlette middleware (Python), pytest-cov (backend coverage), @vitest/coverage-v8 (frontend coverage), web-vitals + posthog-js (frontend performance) + +--- + +## Task 1: Security Headers Middleware + +**Files:** +- Create: `backend/app/core/security_headers.py` +- Modify: `backend/app/core/config.py:49` (add CSP config settings) +- Modify: `backend/app/main.py:236` (register middleware after CORS) + +### Step 1: Write the failing test + +Create `backend/tests/test_security_headers.py`: + +```python +"""Tests for security headers middleware.""" + +import pytest +from httpx import AsyncClient + + +@pytest.mark.asyncio +async def test_security_headers_present(client: AsyncClient): + """Every response should include security headers.""" + response = await client.get("/health") + assert response.status_code == 200 + + # Non-CSP headers always present + assert response.headers["x-content-type-options"] == "nosniff" + assert response.headers["x-frame-options"] == "DENY" + assert response.headers["referrer-policy"] == "strict-origin-when-cross-origin" + assert "camera=()" in response.headers["permissions-policy"] + assert "microphone=()" in response.headers["permissions-policy"] + assert "geolocation=()" in response.headers["permissions-policy"] + + +@pytest.mark.asyncio +async def test_csp_report_only_header(client: AsyncClient): + """CSP should be in report-only mode.""" + response = await client.get("/health") + assert response.status_code == 200 + + csp = response.headers.get("content-security-policy-report-only") + assert csp is not None + assert "default-src 'self'" in csp + assert "script-src 'self'" in csp + assert "style-src 'self' 'unsafe-inline'" in csp + assert "frame-ancestors 'none'" in csp + + +@pytest.mark.asyncio +async def test_hsts_only_in_production(client: AsyncClient): + """HSTS should NOT be sent when DEBUG=true (test environment).""" + response = await client.get("/health") + assert response.status_code == 200 + assert "strict-transport-security" not in response.headers +``` + +### Step 2: Run test to verify it fails + +```bash +cd backend && python -m pytest tests/test_security_headers.py -v +``` + +Expected: FAIL — headers not present yet. + +### Step 3: Add CSP config to settings + +Modify `backend/app/core/config.py`. Add after the `BCRYPT_ROUNDS` line (line 50): + +```python + # Security Headers + CSP_REPORT_ONLY: bool = True # Set False to enforce CSP + CSP_EXTRA_SCRIPT_SOURCES: list[str] = [] # Additional script-src domains + CSP_EXTRA_CONNECT_SOURCES: list[str] = [] # Additional connect-src domains +``` + +### Step 4: Write the middleware + +Create `backend/app/core/security_headers.py`: + +```python +""" +Security headers middleware. + +Adds standard security headers to every HTTP response: +- HSTS (production only) +- X-Content-Type-Options +- X-Frame-Options +- Referrer-Policy +- Permissions-Policy +- Content-Security-Policy (report-only by default) +""" + +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import Response +from typing import Callable + +from app.core.config import settings + + +def _build_csp_directive() -> str: + """Build the CSP directive string from config.""" + script_sources = "'self' https://us.i.posthog.com https://*.sentry.io" + if settings.CSP_EXTRA_SCRIPT_SOURCES: + script_sources += " " + " ".join(settings.CSP_EXTRA_SCRIPT_SOURCES) + + connect_sources = ( + "'self' https://us.posthog.com https://us.i.posthog.com " + "https://*.sentry.io https://api.resolutionflow.com" + ) + if settings.CSP_EXTRA_CONNECT_SOURCES: + connect_sources += " " + " ".join(settings.CSP_EXTRA_CONNECT_SOURCES) + + return "; ".join([ + "default-src 'self'", + f"script-src {script_sources}", + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", + "font-src 'self' https://fonts.gstatic.com", + "img-src 'self' data: blob:", + f"connect-src {connect_sources}", + "frame-ancestors 'none'", + "base-uri 'self'", + "form-action 'self'", + ]) + + +class SecurityHeadersMiddleware(BaseHTTPMiddleware): + """Add security headers to every response.""" + + def __init__(self, app): + super().__init__(app) + # Pre-build CSP at startup so we don't rebuild per-request + self._csp = _build_csp_directive() + + async def dispatch(self, request: Request, call_next: Callable) -> Response: + response = await call_next(request) + + # Always set these headers + response.headers["X-Content-Type-Options"] = "nosniff" + response.headers["X-Frame-Options"] = "DENY" + response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" + response.headers["Permissions-Policy"] = ( + "camera=(), microphone=(), geolocation=()" + ) + + # HSTS only in production (avoid locking localhost into HTTPS) + if not settings.DEBUG: + response.headers["Strict-Transport-Security"] = ( + "max-age=31536000; includeSubDomains" + ) + + # CSP — report-only or enforcing based on config + csp_header = ( + "Content-Security-Policy-Report-Only" + if settings.CSP_REPORT_ONLY + else "Content-Security-Policy" + ) + response.headers[csp_header] = self._csp + + return response +``` + +### Step 5: Register middleware in main.py + +Modify `backend/app/main.py`. Add import at top with other core imports (after line 30): + +```python +from app.core.security_headers import SecurityHeadersMiddleware +``` + +Add middleware registration after the CORS block (after line 235, before `# Include API router`): + +```python +# Add security headers middleware (after CORS so preflight responses work) +app.add_middleware(SecurityHeadersMiddleware) +``` + +### Step 6: Run tests to verify they pass + +```bash +cd backend && python -m pytest tests/test_security_headers.py -v +``` + +Expected: 3 tests PASS. + +### Step 7: Run full backend test suite + +```bash +cd backend && python -m pytest --override-ini="addopts=" -v +``` + +Expected: All tests pass (security headers don't break existing tests). + +### Step 8: Commit + +```bash +git add backend/app/core/security_headers.py backend/app/core/config.py backend/app/main.py backend/tests/test_security_headers.py +git commit -m "feat: add security headers middleware with report-only CSP + +Co-Authored-By: Claude Opus 4.6 (1M context) " +``` + +--- + +## Task 2: Backend Coverage Gate (80%) + +**Files:** +- Modify: `.github/workflows/ci.yml:51` (add --cov-fail-under=80) + +### Step 1: Update the pytest CI command + +Modify `.github/workflows/ci.yml` line 51. Change: + +```yaml + - name: Run tests with coverage + run: cd backend && python -m pytest --override-ini="addopts=" --cov=app --cov-report=term-missing --cov-report=json:coverage.json +``` + +To: + +```yaml + - name: Run tests with coverage + run: cd backend && python -m pytest --override-ini="addopts=" --cov=app --cov-report=term-missing --cov-report=json:coverage.json --cov-fail-under=80 +``` + +### Step 2: Verify locally that coverage is above 80% + +```bash +cd backend && python -m pytest --override-ini="addopts=" --cov=app --cov-report=term-missing --cov-fail-under=80 +``` + +Expected: PASS with total coverage >= 80%. + +### Step 3: Commit + +```bash +git add .github/workflows/ci.yml +git commit -m "ci: enforce 80% backend coverage gate + +Co-Authored-By: Claude Opus 4.6 (1M context) " +``` + +--- + +## Task 3: Frontend Coverage Reporting + +**Files:** +- Modify: `frontend/package.json` (add test:coverage script) +- Modify: `frontend/vite.config.ts:31-36` (add coverage config) +- Modify: `.github/workflows/ci.yml:93` (change npm test → npm run test:coverage) +- Modify: `.gitignore` (add coverage/) + +### Step 1: Install @vitest/coverage-v8 + +```bash +cd frontend && npm install -D @vitest/coverage-v8 +``` + +### Step 2: Add coverage config to vite.config.ts + +Modify `frontend/vite.config.ts`. Replace the `test` block (lines 31-36) with: + +```typescript + test: { + globals: true, + environment: 'jsdom', + setupFiles: './src/test/setup.ts', + include: ['src/**/*.{test,spec}.{ts,tsx}'], + coverage: { + provider: 'v8', + reporter: ['text', 'json-summary', 'html'], + include: ['src/**/*.{ts,tsx}'], + exclude: [ + 'src/test/**', + 'src/types/**', + 'src/**/*.d.ts', + 'src/instrument.ts', + 'src/main.tsx', + ], + }, + }, +``` + +### Step 3: Add test:coverage script to package.json + +Modify `frontend/package.json`. Add after the `"test"` script (line 14): + +```json + "test:coverage": "vitest run --coverage", +``` + +### Step 4: Add coverage/ to .gitignore + +Modify `.gitignore`. Add after the `frontend/e2e/.auth/` line (line 221): + +``` +frontend/coverage/ +``` + +### Step 5: Update CI to use test:coverage + +Modify `.github/workflows/ci.yml` line 93. Change: + +```yaml + - name: Test + run: cd frontend && npm test +``` + +To: + +```yaml + - name: Test with coverage + run: cd frontend && npm run test:coverage +``` + +### Step 6: Verify locally + +```bash +cd frontend && npm run test:coverage +``` + +Expected: Tests pass and coverage summary prints to terminal. Note the baseline percentage. + +### Step 7: Commit + +```bash +git add frontend/package.json frontend/package-lock.json frontend/vite.config.ts .github/workflows/ci.yml .gitignore +git commit -m "ci: add frontend coverage reporting via vitest/v8 + +Co-Authored-By: Claude Opus 4.6 (1M context) " +``` + +--- + +## Task 4: Web Vitals → PostHog + +**Files:** +- Create: `frontend/src/lib/webVitals.ts` +- Modify: `frontend/src/main.tsx:23` (call initWebVitals after PostHog init) + +### Step 1: Install web-vitals + +```bash +cd frontend && npm install web-vitals +``` + +### Step 2: Create the web vitals module + +Create `frontend/src/lib/webVitals.ts`: + +```typescript +/** + * Core Web Vitals reporting via PostHog. + * + * Tracks LCP, INP, CLS, FCP, and TTFB as PostHog events so we can + * build dashboards and correlate performance with product usage. + * + * Each metric fires once per page load. PostHog captures 100% of + * sessions (vs Sentry's 20% sample), giving better coverage. + */ +import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals' +import type { Metric } from 'web-vitals' +import posthog from 'posthog-js' + +function sendToPostHog(metric: Metric) { + // Only send if PostHog is loaded + if (!(posthog as unknown as { __loaded?: boolean }).__loaded) return + + posthog.capture('web_vitals', { + metric_name: metric.name, + metric_value: metric.value, + metric_rating: metric.rating, + metric_delta: metric.delta, + metric_id: metric.id, + page_path: window.location.pathname, + }) +} + +/** Register all Web Vitals observers. Call once after PostHog init. */ +export function initWebVitals() { + onLCP(sendToPostHog) + onINP(sendToPostHog) + onCLS(sendToPostHog) + onFCP(sendToPostHog) + onTTFB(sendToPostHog) +} +``` + +### Step 3: Wire into main.tsx + +Modify `frontend/src/main.tsx`. Add import after the PostHog import (after line 8): + +```typescript +import { initWebVitals } from './lib/webVitals' +``` + +Add the init call after the PostHog init block (after line 23, before `createRoot`): + +```typescript +// Start Web Vitals reporting to PostHog +initWebVitals() +``` + +### Step 4: Verify the build + +```bash +cd frontend && npm run build +``` + +Expected: Build succeeds with no errors. + +### Step 5: Commit + +```bash +git add frontend/src/lib/webVitals.ts frontend/src/main.tsx frontend/package.json frontend/package-lock.json +git commit -m "feat: add Core Web Vitals reporting to PostHog + +Co-Authored-By: Claude Opus 4.6 (1M context) " +``` + +--- + +## Task 5: Update stack priorities plan and verify + +**Files:** +- Modify: `docs/plans/2026-03-16-stack-priorities-and-playwright-plan.md` (update completion status) + +### Step 1: Update the completion status table + +In `docs/plans/2026-03-16-stack-priorities-and-playwright-plan.md`, update these rows: + +```markdown +| Coverage gates in CI | ✅ Complete | Backend enforced at 80%, frontend coverage reporting enabled (no gate yet) | +| Security headers | ✅ Complete | HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, CSP report-only | +| Web Vitals / performance budgets | ✅ Complete | LCP, INP, CLS, FCP, TTFB reported to PostHog via web-vitals library | +``` + +### Step 2: Run the full test suite one more time + +```bash +cd backend && python -m pytest --override-ini="addopts=" --cov=app --cov-report=term-missing --cov-fail-under=80 +cd frontend && npm run test:coverage +cd frontend && npm run build +``` + +Expected: All pass. + +### Step 3: Commit + +```bash +git add docs/plans/2026-03-16-stack-priorities-and-playwright-plan.md +git commit -m "docs: mark security headers, coverage gates, and web vitals complete + +Co-Authored-By: Claude Opus 4.6 (1M context) " +``` From 2f18056fd19d875e2785c0cef98ac01999ebb3aa Mon Sep 17 00:00:00 2001 From: chihlasm Date: Wed, 18 Mar 2026 02:38:42 +0000 Subject: [PATCH 3/7] feat: add security headers middleware with report-only CSP Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/core/config.py | 5 ++ backend/app/core/security_headers.py | 80 ++++++++++++++++++++++++++ backend/app/main.py | 4 ++ backend/tests/test_security_headers.py | 41 +++++++++++++ 4 files changed, 130 insertions(+) create mode 100644 backend/app/core/security_headers.py create mode 100644 backend/tests/test_security_headers.py diff --git a/backend/app/core/config.py b/backend/app/core/config.py index ee0ea9db..ea17db51 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -49,6 +49,11 @@ class Settings(BaseSettings): # Security BCRYPT_ROUNDS: int = 12 + # Security Headers + CSP_REPORT_ONLY: bool = True # Set False to enforce CSP + CSP_EXTRA_SCRIPT_SOURCES: list[str] = [] # Additional script-src domains + CSP_EXTRA_CONNECT_SOURCES: list[str] = [] # Additional connect-src domains + # Registration REQUIRE_INVITE_CODE: bool = True # Set to False to allow open registration diff --git a/backend/app/core/security_headers.py b/backend/app/core/security_headers.py new file mode 100644 index 00000000..5ef552f7 --- /dev/null +++ b/backend/app/core/security_headers.py @@ -0,0 +1,80 @@ +""" +Security headers middleware. + +Adds standard security headers to every HTTP response: +- HSTS (production only) +- X-Content-Type-Options +- X-Frame-Options +- Referrer-Policy +- Permissions-Policy +- Content-Security-Policy (report-only by default) +""" + +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import Response +from typing import Callable + +from app.core.config import settings + + +def _build_csp_directive() -> str: + """Build the CSP directive string from config.""" + script_sources = "'self' https://us.i.posthog.com https://*.sentry.io" + if settings.CSP_EXTRA_SCRIPT_SOURCES: + script_sources += " " + " ".join(settings.CSP_EXTRA_SCRIPT_SOURCES) + + connect_sources = ( + "'self' https://us.posthog.com https://us.i.posthog.com " + "https://*.sentry.io https://api.resolutionflow.com" + ) + if settings.CSP_EXTRA_CONNECT_SOURCES: + connect_sources += " " + " ".join(settings.CSP_EXTRA_CONNECT_SOURCES) + + return "; ".join([ + "default-src 'self'", + f"script-src {script_sources}", + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", + "font-src 'self' https://fonts.gstatic.com", + "img-src 'self' data: blob:", + f"connect-src {connect_sources}", + "frame-ancestors 'none'", + "base-uri 'self'", + "form-action 'self'", + ]) + + +class SecurityHeadersMiddleware(BaseHTTPMiddleware): + """Add security headers to every response.""" + + def __init__(self, app): + super().__init__(app) + # Pre-build CSP at startup so we don't rebuild per-request + self._csp = _build_csp_directive() + + async def dispatch(self, request: Request, call_next: Callable) -> Response: + response = await call_next(request) + + # Always set these headers + response.headers["X-Content-Type-Options"] = "nosniff" + response.headers["X-Frame-Options"] = "DENY" + response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" + response.headers["Permissions-Policy"] = ( + "camera=(), microphone=(), geolocation=()" + ) + + # HSTS only in production (avoid locking localhost into HTTPS) + if not settings.DEBUG: + response.headers["Strict-Transport-Security"] = ( + "max-age=31536000; includeSubDomains" + ) + + # CSP — report-only or enforcing based on config + csp_header = ( + "Content-Security-Policy-Report-Only" + if settings.CSP_REPORT_ONLY + else "Content-Security-Policy" + ) + response.headers[csp_header] = self._csp + + return response diff --git a/backend/app/main.py b/backend/app/main.py index 61e6a4e7..b5f310f3 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -28,6 +28,7 @@ if settings.SENTRY_DSN: from app.core.database import init_db, async_session_maker from app.core.logging_config import setup_logging from app.core.middleware import RequestLoggingMiddleware, ErrorLoggingMiddleware +from app.core.security_headers import SecurityHeadersMiddleware from app.core.rate_limit import limiter from app.api.router import api_router from app.core.scheduler import scheduler, load_all_schedules, _cleanup_expired_ai_conversations @@ -234,6 +235,9 @@ else: expose_headers=["X-Redaction-Mode", "X-Redaction-Summary"], ) +# Add security headers middleware (after CORS so preflight responses work) +app.add_middleware(SecurityHeadersMiddleware) + # Include API router app.include_router(api_router, prefix=settings.API_V1_PREFIX) diff --git a/backend/tests/test_security_headers.py b/backend/tests/test_security_headers.py new file mode 100644 index 00000000..81dbfb43 --- /dev/null +++ b/backend/tests/test_security_headers.py @@ -0,0 +1,41 @@ +"""Tests for security headers middleware.""" + +import pytest +from httpx import AsyncClient + + +@pytest.mark.asyncio +async def test_security_headers_present(client: AsyncClient): + """Every response should include security headers.""" + response = await client.get("/health") + assert response.status_code == 200 + + # Non-CSP headers always present + assert response.headers["x-content-type-options"] == "nosniff" + assert response.headers["x-frame-options"] == "DENY" + assert response.headers["referrer-policy"] == "strict-origin-when-cross-origin" + assert "camera=()" in response.headers["permissions-policy"] + assert "microphone=()" in response.headers["permissions-policy"] + assert "geolocation=()" in response.headers["permissions-policy"] + + +@pytest.mark.asyncio +async def test_csp_report_only_header(client: AsyncClient): + """CSP should be in report-only mode.""" + response = await client.get("/health") + assert response.status_code == 200 + + csp = response.headers.get("content-security-policy-report-only") + assert csp is not None + assert "default-src 'self'" in csp + assert "script-src 'self'" in csp + assert "style-src 'self' 'unsafe-inline'" in csp + assert "frame-ancestors 'none'" in csp + + +@pytest.mark.asyncio +async def test_hsts_only_in_production(client: AsyncClient): + """HSTS should NOT be sent when DEBUG=true (test environment).""" + response = await client.get("/health") + assert response.status_code == 200 + assert "strict-transport-security" not in response.headers From b1d1aef702f7fd2a19a558cc32c01b5d1ab1c96e Mon Sep 17 00:00:00 2001 From: chihlasm Date: Wed, 18 Mar 2026 02:41:00 +0000 Subject: [PATCH 4/7] ci: enforce 80% backend coverage gate and add frontend coverage reporting Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 6 +++--- .gitignore | 1 + frontend/package.json | 3 +++ frontend/vite.config.ts | 12 ++++++++++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63630367..bf17e58a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: run: pip install -r backend/requirements.txt -r backend/requirements-dev.txt - name: Run tests with coverage - run: cd backend && python -m pytest --override-ini="addopts=" --cov=app --cov-report=term-missing --cov-report=json:coverage.json + run: cd backend && python -m pytest --override-ini="addopts=" --cov=app --cov-report=term-missing --cov-report=json:coverage.json --cov-fail-under=80 - name: Display coverage summary if: always() @@ -89,8 +89,8 @@ jobs: - name: Lint run: cd frontend && npm run lint - - name: Test - run: cd frontend && npm test + - name: Test with coverage + run: cd frontend && npm run test:coverage - name: Build run: cd frontend && npm run build diff --git a/.gitignore b/.gitignore index cfb5aab2..fbe1c9e2 100644 --- a/.gitignore +++ b/.gitignore @@ -219,6 +219,7 @@ frontend/stats.html frontend/playwright-report/ frontend/test-results/ frontend/e2e/.auth/ +frontend/coverage/ # Superpowers brainstorming mockups .superpowers/ diff --git a/frontend/package.json b/frontend/package.json index ae7e4618..721a2590 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "lint": "eslint .", "preview": "vite preview", "test": "vitest run", + "test:coverage": "vitest run --coverage", "test:watch": "vitest", "test:e2e": "playwright test", "test:e2e:headed": "playwright test --headed", @@ -48,6 +49,7 @@ "recharts": "^3.7.0", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", + "web-vitals": "^4.2.4", "zundo": "^2.3.0", "zustand": "^5.0.10" }, @@ -60,6 +62,7 @@ "@types/node": "^24.10.9", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", + "@vitest/coverage-v8": "^4.0.18", "@vitejs/plugin-react": "^5.1.1", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 38c64084..de98c4f0 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -33,6 +33,18 @@ export default defineConfig({ environment: 'jsdom', setupFiles: './src/test/setup.ts', include: ['src/**/*.{test,spec}.{ts,tsx}'], + coverage: { + provider: 'v8', + reporter: ['text', 'json-summary', 'html'], + include: ['src/**/*.{ts,tsx}'], + exclude: [ + 'src/test/**', + 'src/types/**', + 'src/**/*.d.ts', + 'src/instrument.ts', + 'src/main.tsx', + ], + }, }, build: { sourcemap: 'hidden', // Generate source maps but don't expose them publicly From 9b3b82882e950295ee7d2eefb50e3314d5c650e6 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Wed, 18 Mar 2026 02:41:43 +0000 Subject: [PATCH 5/7] feat: add Core Web Vitals reporting to PostHog Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/lib/webVitals.ts | 35 +++++++++++++++++++++++++++++++++++ frontend/src/main.tsx | 4 ++++ 2 files changed, 39 insertions(+) create mode 100644 frontend/src/lib/webVitals.ts diff --git a/frontend/src/lib/webVitals.ts b/frontend/src/lib/webVitals.ts new file mode 100644 index 00000000..d4427b0b --- /dev/null +++ b/frontend/src/lib/webVitals.ts @@ -0,0 +1,35 @@ +/** + * Core Web Vitals reporting via PostHog. + * + * Tracks LCP, INP, CLS, FCP, and TTFB as PostHog events so we can + * build dashboards and correlate performance with product usage. + * + * Each metric fires once per page load. PostHog captures 100% of + * sessions (vs Sentry's 20% sample), giving better coverage. + */ +import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals' +import type { Metric } from 'web-vitals' +import posthog from 'posthog-js' + +function sendToPostHog(metric: Metric) { + // Only send if PostHog is loaded + if (!(posthog as unknown as { __loaded?: boolean }).__loaded) return + + posthog.capture('web_vitals', { + metric_name: metric.name, + metric_value: metric.value, + metric_rating: metric.rating, + metric_delta: metric.delta, + metric_id: metric.id, + page_path: window.location.pathname, + }) +} + +/** Register all Web Vitals observers. Call once after PostHog init. */ +export function initWebVitals() { + onLCP(sendToPostHog) + onINP(sendToPostHog) + onCLS(sendToPostHog) + onFCP(sendToPostHog) + onTTFB(sendToPostHog) +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 28272af2..cb21d7c9 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -6,6 +6,7 @@ import { createRoot } from 'react-dom/client' import { reactErrorHandler } from '@sentry/react' import { HelmetProvider } from 'react-helmet-async' import { PostHogProvider } from '@posthog/react' +import { initWebVitals } from './lib/webVitals' import { Toaster } from 'sonner' import './index.css' import App from './App' @@ -22,6 +23,9 @@ if (posthogKey) { }) } +// Start Web Vitals reporting to PostHog +initWebVitals() + createRoot(document.getElementById('root')!, { onUncaughtError: reactErrorHandler(), onCaughtError: reactErrorHandler(), From 6141ab5a33deab7468fedcb70d2c07821400744b Mon Sep 17 00:00:00 2001 From: chihlasm Date: Wed, 18 Mar 2026 02:42:09 +0000 Subject: [PATCH 6/7] docs: mark security headers, coverage gates, and web vitals complete Co-Authored-By: Claude Opus 4.6 (1M context) --- 03-DEVELOPMENT-ROADMAP.md | 109 +++++++++++------- ...16-stack-priorities-and-playwright-plan.md | 21 ++++ 2 files changed, 91 insertions(+), 39 deletions(-) diff --git a/03-DEVELOPMENT-ROADMAP.md b/03-DEVELOPMENT-ROADMAP.md index d4ffcf53..bd226221 100644 --- a/03-DEVELOPMENT-ROADMAP.md +++ b/03-DEVELOPMENT-ROADMAP.md @@ -1,6 +1,6 @@ # Development Roadmap -> **Last Updated:** March 1, 2026 +> **Last Updated:** March 18, 2026 > **Product:** ResolutionFlow (repo: patherly) > **Target Market:** MSP companies — IT service providers managing infrastructure and support for multiple clients @@ -27,17 +27,44 @@ - Mobile-responsive design - Security hardening (Phases A-D) — rate limiting, audit logs, soft delete, SQL escaping -### Phase 2.5: Step Library & Procedural Flows (Partially Complete) -- **Step Library backend** — CRUD, search, ratings, reviews, visibility filtering, verified-use badge -- **Procedural flows** — `procedural` tree type, step-by-step execution engine, intake forms, section headers, resume support, type-aware routing +### Phase 2.5: Step Library & Procedural Flows +- **Step Library** — CRUD, search, ratings, reviews, visibility filtering, verified-use badge (backend complete, frontend browse/search/rate pending) +- **Procedural flows** — `procedural` tree type, step-by-step execution engine, intake forms, section headers, resume support, type-aware routing, auto-start on page load - **Maintenance flows** — `maintenance` tree type, batch session launch, saved target lists, APScheduler cron scheduling, maintenance detail page - **Session sharing** — ShareSessionModal, SharedSessionPage, MySharesPage, share links with copy/manage - **Export improvements** (Phases A-C) — step cutoff, summary block, detail levels, editable preview, sensitive data redaction -- **Rebrand** — Patherly → ResolutionFlow branding, dark-first purple gradient design system +- **Rebrand** — Patherly → ResolutionFlow branding, Slate & Ice glassmorphism design system +- **Flexible intake** — deferred variable resolution, prepared sessions for reuse -### Recently Completed (Feb-Mar 2026) -- **AI Flow Assist** — Conversational AI chat builder for generating troubleshooting and procedural flows. Multi-phase interview (scope → structure → details), progressive tree generation with live preview, save to flow library. Backend: Anthropic Claude API with streaming, AI tree validation, scaffold/refine pipeline. Frontend: ChatPanel, StaticTreePreview, ChatToolbar, Zustand store. -- **Cross-Reference / Loop-Back Support** — Ghost references allowing any node to link to any other node in the tree (loop-backs, re-verification patterns). Backend validation relaxed for cross-refs. Frontend: cross-reference edge rendering (dashed purple arrows), node picker dropdowns in action/decision forms, circular reference detection changed to warnings. +### Phase 3: Intelligence & Polish (Mar 2026) +- **AI Flow Assist** — Conversational AI chat builder for generating troubleshooting and procedural flows. Multi-phase interview, progressive tree generation with live preview, save to flow library +- **Cross-Reference / Loop-Back Support** — Ghost references, dashed purple arrows, node picker dropdowns, circular reference detection as warnings +- **Editor-Embedded Flow Assist** — 320px AI side panel in editor, ghost node suggestions, delta responses, model tier routing +- **Procedural Flow Assist** — AI suggestions for procedural flow editing with `[STEPS_UPDATE]` markers +- **AI Chat Conclusion** — Outcome tracking, AI-generated ticket summaries, resume flow +- **AI Copilot** — In-session copilot panel with RAG, standalone assistant chat +- **KB Accelerator** — Upload .md files and convert to troubleshooting/procedural flows with AI, tree builder with validation gate +- **Script Generator** — Backend engine with parameter detection, template editor with conditional/looping, PowerShell and Bash generation +- **Command Palette** — Quick flow navigation and actions (Ctrl+K) +- **Session-to-Flow Converter** — AI-powered conversion of finished sessions into reusable flows +- **Sidebar Redesign** — Activity feed, grouped navigation, improved flow discovery +- **Flow Export/Import** — JSON export, import with validation, cross-team flow transfer +- **Survey System** — Public survey page, admin invite tracking, response viewer with CSV export, email invitations, read/unread/archive/delete management +- **Email Verification** — Tokens, banner, admin toggle (platform setting) +- **Account Management** — Profile settings, delete/leave/transfer team, chat retention settings +- **Slate & Ice Aesthetic Redesign** — Glassmorphism with ice-cyan accents, Bricolage Grotesque + IBM Plex Sans + JetBrains Mono typography, orchestrated page-load animations +- **Tailwind CSS v3 → v4 Migration** — CSS custom properties, glass utilities, React Flow CSS integration +- **Landing Page + Beta Signup** — Marketing-ready landing page with CSS polish +- **PostHog Product Analytics** — 60+ events instrumented, PostHogProvider, identifyUser/resetAnalytics wired to auth, Dockerfile build args for Railway +- **Playwright E2E Tests** — 17 spec files, full CI job, auth storage state, both webServers managed in config +- **ConnectWise PSA Integration (Core)** — Provider pattern (`BasePsaProvider` / `ConnectWiseProvider`), connection CRUD, ticket context retrieval, session-to-ticket linking, member mapping, credential encryption, in-memory TTL cache + +### Phase 3.5: Polish & Professional (Mar 2026 — PR #114) +- **Empty States** — Illustrative empty states across 8 pages, upgraded EmptyState component with illustration + learn-more support +- **Onboarding Checklist** — Backend status/dismiss endpoints, dashboard checklist widget with structured steps +- **Professional PDF Export** — WeasyPrint branded template, supporting data in all export formats +- **Team Branding** — CRUD endpoints + UI settings for export customization +- **Supporting Data Capture** — CRUD endpoints + UI for attaching evidence/context to sessions --- @@ -45,14 +72,21 @@ | Task | Status | Notes | |------|--------|-------| -| Step Library Frontend UI | In Progress | Backend complete, frontend browse/search/rate UI pending | -| Procedural Flows Lifecycle | In Progress | Resume done, run chooser/reuse pending | +| ConnectWise PSA Integration (Advanced) | In Progress | Core done — ticket linking, note posting, member mapping. Remaining: callback webhooks, deeper ticket context in sessions | +| PR #114 Merge | In Progress | Empty states, onboarding, PDF exports, branding, supporting data — ready for review | --- -## Phase 3: Intelligence & Polish +## What's Next -**Goal:** Make ResolutionFlow smarter — surface insights from usage data and make the day-to-day experience faster. +### Near-Term Priorities (from Stack Priorities Plan) + +| Feature | Status | Description | +|---------|--------|-------------| +| Coverage gates in CI | ✅ Complete | Backend enforced at 80%, frontend coverage reporting enabled | +| Security headers | ✅ Complete | HSTS, CSP (report-only), X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy | +| Web Vitals / performance budgets | ✅ Complete | LCP, INP, CLS, FCP, TTFB reported to PostHog via web-vitals | +| Search and recall improvements | ⬜ Not started | Search sessions by flow, tag, client, ticket context | ### 3A: Quick Wins & UX (Priority: Medium) @@ -69,12 +103,6 @@ | Tree Effectiveness Dashboard | #61 | Usage stats, common paths, avg completion time, success rates per flow | | Recurring Issue Detection | #60 | Identify repeat problems across sessions — surface patterns to team leads | -### 3C: Content Management (Priority: Medium) - -| Feature | GitHub Issue | Description | -|---------|-------------|-------------| -| Tree Templates + Import/Export | #66 | Starter templates, JSON/YAML export, community sharing foundation | - ### 3D: Remaining Infrastructure - File attachments for sessions (S3-compatible storage, drag-and-drop, screenshot paste) - Tree forking UI (backend schema exists — migration 022) @@ -86,13 +114,12 @@ **Goal:** Connect ResolutionFlow to the MSP tools teams already use. -### 4A: PSA Integration (Priority: HIGH) +### 4A: PSA Integration (Priority: HIGH — Core Complete) | Feature | GitHub Issue | Description | |---------|-------------|-------------| -| ConnectWise / Autotask Integration | #63 | Create tickets from sessions, sync ticket numbers, update notes, pull client context | - -This is the highest-priority strategic feature. It turns ResolutionFlow from a standalone tool into part of the MSP workflow. +| ConnectWise PSA — Advanced | #63 | ~~Core integration~~ ✅ — Remaining: callback webhooks, real-time ticket events, deeper session context | +| Autotask PSA | — | Second PSA provider using same `BasePsaProvider` pattern | ### 4B: Intelligence Layer (Priority: Strategic) @@ -102,7 +129,7 @@ This is the highest-priority strategic feature. It turns ResolutionFlow from a s | Intelligence Loop / Analytics Engine | #65 | Cross-session pattern analysis, auto-suggest flow improvements, team benchmarking | ### 4C: Automation -- PowerShell script execution framework with security sandbox +- PowerShell script execution framework with security sandbox (script generator foundation complete) - Script library management - Automation toggle at action nodes @@ -128,9 +155,10 @@ This is the highest-priority strategic feature. It turns ResolutionFlow from a s | Feature | GitHub Issue | Description | |---------|-------------|-------------| -| AI Copilot — In-Session Intelligence | #69 | Real-time AI suggestions during troubleshooting based on context and history | | Multi-Tree Sessions | #68 | Navigate across multiple flows in a single session, AI-suggested flow transitions | +> **Note:** AI Copilot (#69) completed in Phase 3. Session-to-Flow converter and KB Accelerator also delivered. + ### 5C: Platform Growth - Public API with key management and webhooks - Community tree marketplace @@ -145,23 +173,26 @@ This is the highest-priority strategic feature. It turns ResolutionFlow from a s - Vertical-specific flow libraries (healthcare IT, financial services, education) - Advanced compliance (SOC 2, ISO 27001) - Voice-guided troubleshooting +- Evidence-rich sessions (screenshots, attachments, command output capture) +- Buyer-facing trust surfaces (changelog, status page, security page) +- Queue / worker architecture for AI, indexing, webhook fan-out --- ## Open GitHub Issues Summary -| # | Title | Priority | Phase | -|---|-------|----------|-------| -| #63 | PSA Integration (ConnectWise / Autotask) | HIGH | 4A | -| #70 | Quick Actions Dashboard | Medium | 3A | -| #66 | Tree Templates + Import/Export | Medium | 3C | -| #62 | Quick-Start from Clipboard | Medium | 3A | -| #61 | Tree Effectiveness Dashboard | Medium | 3B | -| #60 | Recurring Issue Detection | Medium | 3B | -| #58 | Step Feedback Flag | UX | 3A | -| #64 | Client Intelligence Sidebar | Strategic | 4B | -| #65 | Intelligence Loop / Analytics Engine | Strategic | 4B | -| #71 | Team Activity Feed + Collaboration | Low | 5A | -| #69 | AI Copilot — In-Session Intelligence | Low | 5B | -| #68 | Multi-Tree Sessions | Low | 5B | -| #67 | Push Steps to Active Sessions | Low | 5A | +| # | Title | Priority | Phase | Status | +|---|-------|----------|-------|--------| +| #63 | PSA Integration (ConnectWise / Autotask) | HIGH | 4A | Core complete, advanced in progress | +| #70 | Quick Actions Dashboard | Medium | 3A | Not started | +| #66 | Tree Templates + Import/Export | Medium | 3C | ✅ Export/Import done | +| #62 | Quick-Start from Clipboard | Medium | 3A | Not started | +| #61 | Tree Effectiveness Dashboard | Medium | 3B | Not started | +| #60 | Recurring Issue Detection | Medium | 3B | Not started | +| #58 | Step Feedback Flag | UX | 3A | Not started | +| #64 | Client Intelligence Sidebar | Strategic | 4B | Not started | +| #65 | Intelligence Loop / Analytics Engine | Strategic | 4B | Not started | +| #71 | Team Activity Feed + Collaboration | Low | 5A | Not started | +| #69 | AI Copilot — In-Session Intelligence | Low | 5B | ✅ Complete | +| #68 | Multi-Tree Sessions | Low | 5B | Not started | +| #67 | Push Steps to Active Sessions | Low | 5A | Not started | diff --git a/docs/plans/2026-03-16-stack-priorities-and-playwright-plan.md b/docs/plans/2026-03-16-stack-priorities-and-playwright-plan.md index d0c946f9..569c2b5f 100644 --- a/docs/plans/2026-03-16-stack-priorities-and-playwright-plan.md +++ b/docs/plans/2026-03-16-stack-priorities-and-playwright-plan.md @@ -1,9 +1,30 @@ # Stack Priorities And Playwright Plan > **Date:** 2026-03-16 +> **Updated:** 2026-03-17 > **Product:** ResolutionFlow > **Purpose:** Turn the recent stack-gap review into a practical, sequenced execution plan +## Completion Status + +| Item | Status | Notes | +|------|--------|-------| +| Product analytics (PostHog) | ✅ Complete | All 9 events tracked, identifyUser/resetAnalytics wired to auth, PostHogProvider in main.tsx | +| Playwright e2e | ✅ Complete | 17 spec files, full CI job, auth storage state, both webServers managed in config | +| Better empty states | ✅ Complete | Illustrative empty states rolled out across 8 pages, upgraded EmptyState component with illustration + learn-more support, 2 new guide entries | +| Onboarding checklist | ✅ Complete | Backend status/dismiss endpoints, dashboard checklist widget with structured steps | +| Professional exports | ✅ Complete | PDF export via WeasyPrint with branded template, supporting data in all export formats, team branding CRUD + UI settings, supporting data capture CRUD + UI | +| Coverage gates in CI | ✅ Complete | Backend enforced at 80%, frontend coverage reporting enabled (no gate yet) | +| Security headers | ✅ Complete | HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, CSP report-only | +| Web Vitals / performance budgets | ✅ Complete | LCP, INP, CLS, FCP, TTFB reported to PostHog via web-vitals library | +| Search and recall improvements | ⬜ Not started | | +| Evidence-rich sessions | ⬜ Not started | | +| Smart PSA / client context | ⬜ Not started | | +| Queue / worker architecture | ⬜ Not started | | +| Buyer-facing trust surfaces | ⬜ Not started | | + +--- + --- ## Summary From d58c3336357e7d2fc87aff9a024f86ecd8099df6 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Wed, 18 Mar 2026 03:08:00 +0000 Subject: [PATCH 7/7] chore: update lockfile with web-vitals and vitest coverage deps Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/package-lock.json | 293 ++++++++++++++++++++++++++++--------- 1 file changed, 227 insertions(+), 66 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6b603c47..16618a66 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -36,6 +36,7 @@ "recharts": "^3.7.0", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", + "web-vitals": "^4.2.4", "zundo": "^2.3.0", "zustand": "^5.0.10" }, @@ -49,6 +50,7 @@ "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", + "@vitest/coverage-v8": "^4.0.18", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", @@ -310,12 +312,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -399,9 +401,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -411,6 +413,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@csstools/color-helpers": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.1.tgz", @@ -3313,18 +3325,49 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/@vitest/expect": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", - "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "node_modules/@vitest/coverage-v8": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.0.tgz", + "integrity": "sha512-nDWulKeik2bL2Va/Wl4x7DLuTKAXa906iRFooIRPR+huHkcvp9QDkPQ2RJdmjOFrqOqvNfoSQLF68deE3xC3CQ==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.0.0", + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.0", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.0", + "vitest": "4.1.0" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "chai": "^6.2.1", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "chai": "^6.2.2", "tinyrainbow": "^3.0.3" }, "funding": { @@ -3332,13 +3375,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", - "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.18", + "@vitest/spy": "4.1.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -3347,7 +3390,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -3359,9 +3402,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", - "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", "dev": true, "license": "MIT", "dependencies": { @@ -3372,13 +3415,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", - "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.18", + "@vitest/utils": "4.1.0", "pathe": "^2.0.3" }, "funding": { @@ -3386,13 +3429,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", - "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -3401,9 +3445,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", - "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", "dev": true, "license": "MIT", "funding": { @@ -3411,13 +3455,14 @@ } }, "node_modules/@vitest/utils": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", - "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" }, "funding": { @@ -3587,6 +3632,25 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4420,9 +4484,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, "license": "MIT" }, @@ -5177,6 +5241,13 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -5452,6 +5523,45 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -5926,6 +6036,47 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/marked": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", @@ -6989,6 +7140,12 @@ "@types/trusted-types": "^2.0.7" } }, + "node_modules/posthog-js/node_modules/web-vitals": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz", + "integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==", + "license": "Apache-2.0" + }, "node_modules/preact": { "version": "10.29.0", "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.0.tgz", @@ -7605,9 +7762,9 @@ "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==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", "dev": true, "license": "MIT" }, @@ -8237,31 +8394,31 @@ } }, "node_modules/vitest": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", - "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.0.18", - "@vitest/mocker": "4.0.18", - "@vitest/pretty-format": "4.0.18", - "@vitest/runner": "4.0.18", - "@vitest/snapshot": "4.0.18", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", - "std-env": "^3.10.0", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -8277,12 +8434,13 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.18", - "@vitest/browser-preview": "4.0.18", - "@vitest/browser-webdriverio": "4.0.18", - "@vitest/ui": "4.0.18", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -8311,6 +8469,9 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, @@ -8328,9 +8489,9 @@ } }, "node_modules/web-vitals": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz", - "integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", "license": "Apache-2.0" }, "node_modules/webidl-conversions": {